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

View File

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

View File

@ -10,6 +10,7 @@ import VueClipboard from 'vue3-clipboard';
import AppUnavailable from '@/AppUnavailable.vue' import AppUnavailable from '@/AppUnavailable.vue'
import router from '@/router'; import router from '@/router';
import { initState, ResourceService, Resource } from "@/api/grpc"; import { initState, ResourceService, Resource } from "@/api/grpc";
import { AuthConfigID, AuthConfigType, DefaultNamespace } from "@/api/resources"; import { AuthConfigID, AuthConfigType, DefaultNamespace } from "@/api/resources";
import { Runtime } from "@/api/common/omni.pb"; 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 { AuthType, authType, suspended } from "@/methods";
import { createAuth0 } from "@auth0/auth0-vue"; import { createAuth0 } from "@auth0/auth0-vue";
import { withRuntime } from "./api/options"; 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 () => { const setupApp = async () => {
let authConfigSpec: AuthConfigSpec | undefined = undefined let authConfigSpec: AuthConfigSpec | undefined = undefined
@ -68,4 +83,4 @@ const setupApp = async () => {
initState(); 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(); const stream: Ref<Stream<R, T> | undefined> = ref();
let buffer: LogLine[] = []; let buffer: LogLine[] = [];
let flush: NodeJS.Timeout | undefined; 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; const p = isRef(params) ? params.value : params;
if (!p) {
return;
}
stream.value = subscribe( stream.value = subscribe(
method, method,
p, 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 { return {
namespace: "system", namespace: "system",
id: service.value, id: service.value,
@ -134,7 +138,7 @@ watch(() => route.params.service, () => {
logParser.setLineParser(getLineParser(svc)); logParser.setLineParser(getLineParser(svc));
service.value = svc; service.value = svc;
}) });
const stream = setupLogStream(logs, MachineService.Logs, params, logParser, withRuntime(Runtime.Talos), withContext(context)); const stream = setupLogStream(logs, MachineService.Logs, params, logParser, withRuntime(Runtime.Talos), withContext(context));

View File

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

View File

@ -102,12 +102,29 @@ func (handler *StaticHandler) serveFile(w http.ResponseWriter, r *http.Request,
return return
} }
defer file.Close() //nolint:errcheck
if path != index { if path != index {
w.Header().Set("Vary", "Accept-Encoding, User-Agent") w.Header().Set("Vary", "Accept-Encoding, User-Agent")
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, immutable", handler.maxAgeSec)) 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) http.ServeContent(w, r, file.Name(), handler.modTime, file)