fix: add more strict security headers to the web page handler
Some checks are pending
default / default (push) Waiting to run
default / e2e-backups (push) Blocked by required conditions
default / e2e-forced-removal (push) Blocked by required conditions
default / e2e-scaling (push) Blocked by required conditions
default / e2e-short (push) Blocked by required conditions
default / e2e-short-secureboot (push) Blocked by required conditions
default / e2e-templates (push) Blocked by required conditions
default / e2e-upgrades (push) Blocked by required conditions
default / e2e-workload-proxy (push) Blocked by required conditions

Enable `CSP`, `Referrer-Policy`, `X-Frame-Options`,
`X-Content-Type-Options`, `Permissions-Policy` headers.

`CSP` has broken the monaco editor, so had to drop the monaco vite
plugin for production builds.

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
This commit is contained in:
Artem Chernyshev 2025-03-18 22:14:06 +03:00
parent 57c005e5d0
commit b91b673a00
No known key found for this signature in database
GPG Key ID: E084A2DF1143C14D
7 changed files with 99 additions and 51 deletions

View File

@ -10,51 +10,51 @@
"lint": "eslint ./src"
},
"dependencies": {
"@auth0/auth0-vue": "^2.3.3",
"@auth0/auth0-vue": "^2.4.0",
"@headlessui/vue": "^1.7.23",
"@jsonforms/vue": "^3.4.1",
"@jsonforms/vue-vanilla": "^3.4.1",
"@kubernetes/client-node": "^0.22.2",
"@jsonforms/vue": "^3.5.1",
"@jsonforms/vue-vanilla": "^3.5.1",
"@kubernetes/client-node": "^0.22.3",
"apexcharts": "3.45.2",
"click-outside-vue3": "^4.0.1",
"core-js": "^3.39.0",
"core-js": "^3.41.0",
"js-yaml": "^4.1.0",
"long": "^5.2.3",
"long": "^5.3.1",
"luxon": "^3.5.0",
"monaco-editor": "^0.41.0",
"monaco-yaml": "^5.2.3",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.3.1",
"pluralize": "^8.0.0",
"rxjs": "^7.8.1",
"semver-parser": "^4.1.6",
"rxjs": "^7.8.2",
"semver-parser": "^4.1.8",
"uuid": "^10.0.0",
"vue": "^3.5.12",
"vue-router": "^4.4.5",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-virtual-scroller": "^2.0.0-beta.8",
"vue-word-highlighter": "^1.2.5",
"vue3-apexcharts": "^1.7.0",
"vue3-apexcharts": "^1.8.0",
"vue3-popper": "^1.5.0"
},
"devDependencies": {
"@heroicons/vue": "^2.1.5",
"@heroicons/vue": "^2.2.0",
"@types/luxon": "^3.4.2",
"@types/node": "^20.17.6",
"@types/node": "^20.17.24",
"@types/pluralize": "^0.0.33",
"@vitejs/plugin-vue": "^5.1.4",
"autoprefixer": "^10.4.20",
"@vitejs/plugin-vue": "^5.2.3",
"autoprefixer": "^10.4.21",
"eslint-plugin-typescript": "^0.14.0",
"eslint-plugin-vue": "^9.30.0",
"eslint-plugin-vue": "^9.33.0",
"fetch-intercept": "^2.4.0",
"jsdom": "^25.0.1",
"lodash": "^4.17.21",
"openpgp": "^5.11.2",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"typescript-eslint": "^8.13.0",
"vite": "^5.4.10",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1",
"vite": "^5.4.14",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vue-tsc": "^2.1.10",
"vue-tsc": "^2.2.8",
"vue3-clipboard": "^1.0.0",
"whatwg-fetch": "^3.6.20"
}

View File

@ -201,4 +201,8 @@ monaco.editor.defineTheme("sidero", {
.editor h4 {
@apply font-bold;
}
.monaco-editor {
outline: 0;
}
</style>

View File

@ -10,6 +10,7 @@ import VueClipboard from 'vue3-clipboard';
import AppUnavailable from '@/AppUnavailable.vue'
import router from '@/router';
import { initState, ResourceService, Resource } from "@/api/grpc";
import { AuthConfigID, AuthConfigType, DefaultNamespace } from "@/api/resources";
import { Runtime } from "@/api/common/omni.pb";
@ -17,7 +18,21 @@ import { AuthConfigSpec } from "@/api/omni/specs/auth.pb";
import { AuthType, authType, suspended } from "@/methods";
import { createAuth0 } from "@auth0/auth0-vue";
import { withRuntime } from "./api/options";
import vClickOutside from "click-outside-vue3"
import vClickOutside from "click-outside-vue3";
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import yamlWorker from 'monaco-yaml/yaml.worker?worker';
if (process.env.NODE_ENV !== 'development') {
(self as any).MonacoEnvironment = {
getWorker(_: string, label: string) {
if (label === 'yaml') {
return new yamlWorker();
}
return new editorWorker();
}
};
}
const setupApp = async () => {
let authConfigSpec: AuthConfigSpec | undefined = undefined
@ -68,4 +83,4 @@ const setupApp = async () => {
initState();
setupApp()
setupApp();

View File

@ -75,7 +75,7 @@ export class LineDelimitedLogParser {
}
}
export const setupLogStream = <R extends Data, T>(logs: Ref<LogLine[]>, method: StreamingRequest<R, T>, params: T | ComputedRef<T> | Ref<T>, logParser: LogParser = new DefaultLogParser((l: string): LogLine => { return {msg: l} }), ...options: fetchOption[]): Ref<Stream<R, T> | undefined> => {
export const setupLogStream = <R extends Data, T>(logs: Ref<LogLine[]>, method: StreamingRequest<R, T>, params: T | ComputedRef<T | undefined> | Ref<T>, logParser: LogParser = new DefaultLogParser((l: string): LogLine => { return {msg: l} }), ...options: fetchOption[]): Ref<Stream<R, T> | undefined> => {
const stream: Ref<Stream<R, T> | undefined> = ref();
let buffer: LogLine[] = [];
let flush: NodeJS.Timeout | undefined;
@ -97,6 +97,10 @@ export const setupLogStream = <R extends Data, T>(logs: Ref<LogLine[]>, method:
const p = isRef(params) ? params.value : params;
if (!p) {
return;
}
stream.value = subscribe(
method,
p,

View File

@ -108,7 +108,11 @@ const plainText = (line: string) => {
};
}
const params = computed<LogsRequest>(() => {
const params = computed<LogsRequest | undefined>(() => {
if (route.params.service === "machine") {
return;
}
return {
namespace: "system",
id: service.value,
@ -134,7 +138,7 @@ watch(() => route.params.service, () => {
logParser.setLineParser(getLineParser(svc));
service.value = svc;
})
});
const stream = setupLogStream(logs, MachineService.Logs, params, logParser, withRuntime(Runtime.Talos), withContext(context));

View File

@ -5,7 +5,7 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import { defineConfig, UserConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
@ -13,11 +13,25 @@ import monacoEditorPlugin from 'vite-plugin-monaco-editor'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
nodePolyfills({ include: ['stream'] }),
monacoEditorPlugin({
export default defineConfig(({ command }) => {
const config: UserConfig = {
plugins: [
vue(),
nodePolyfills({ include: ['stream'] }),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
}
},
server: {
port: 8121,
host: "127.0.0.1"
},
};
if (command === 'serve') {
config.plugins?.push(monacoEditorPlugin({
languageWorkers: ['editorWorkerService'],
customWorkers: [
{
@ -25,18 +39,8 @@ export default defineConfig({
entry: 'monaco-yaml/yaml.worker'
}
]
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
}
},
server: {
port: 8121,
host: "127.0.0.1"
},
test: {
exclude: [],
},
}));
}
return config;
})

View File

@ -102,12 +102,29 @@ func (handler *StaticHandler) serveFile(w http.ResponseWriter, r *http.Request,
return
}
defer file.Close() //nolint:errcheck
if path != index {
w.Header().Set("Vary", "Accept-Encoding, User-Agent")
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, immutable", handler.maxAgeSec))
}
} else {
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
defer file.Close() //nolint:errcheck
w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src * data: ; "+
";connect-src 'self' https://*.auth0.com ;font-src 'self' data: "+
";style-src 'self' 'unsafe-inline' https://fonts.googleapis.com data: ;upgrade-insecure-requests;"+
";frame-src https://*.auth0.com",
)
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), "+
"autoplay=(self), battery=(), camera=(), cross-origin-isolated=(self), display-capture=(), "+
"document-domain=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), "+
"magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials=(self),"+
"screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=()",
)
}
http.ServeContent(w, r, file.Name(), handler.modTime, file)