From 2a18604b25577769c915e24325692d4f5ed2d4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Geel=20Weirs=C3=B8e?= Date: Thu, 8 Oct 2026 21:47:12 +0200 Subject: [PATCH] Require an approval stamp on every blog post before building Posts are written in a private drafts repo and only reach this repo through an approval step that stamps them with an HMAC over path and content. The deploy workflow now runs scripts/verify-approvals.mjs before the build and refuses to build if a post is unstamped, changed after approval, or still marked as a draft (this repo is public, so a draft here is already published). PW_PREVIEW=1 builds the site under /udkast for the private review page: noindex, a banner, no service worker, sitemap or comments. The nine existing posts are stamped as approved. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu --- .gitea/workflows/deploy.yml | 6 ++ astro.config.mjs | 7 +- scripts/verify-approvals.mjs | 65 +++++++++++++++++++ src/consts.ts | 3 + src/content.config.ts | 2 + src/content/blog/ah-fail-skaermlaas-rust.md | 1 + src/content/blog/authentik-oidc-faldgruber.md | 1 + .../authentik-reverse-proxy-forward-auth.md | 1 + src/content/blog/er-mit-homelab-opdateret.md | 1 + .../inject-javascript-npmplus-sub-filter.md | 1 + src/content/blog/npm-til-npmplus.md | 1 + src/content/blog/proxmox-hjemmelab.md | 1 + src/content/blog/proxmox-lxc-fejl.md | 1 + src/content/blog/velkommen.md | 1 + src/layouts/Base.astro | 11 +++- src/pages/blog/[...slug].astro | 6 +- 16 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 scripts/verify-approvals.mjs diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 4cfd98c..cee780e 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -35,6 +35,12 @@ jobs: npm install --include=optional --os=linux --libc=glibc --cpu=x64 sharp node -e "console.log('sharp', require('sharp').versions.sharp)" + # Intet indlæg bygges uden godkendelse; se scripts/verify-approvals.mjs. + - name: Verify blog approvals + env: + PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }} + run: node scripts/verify-approvals.mjs + - name: Build site run: npm run build diff --git a/astro.config.mjs b/astro.config.mjs index 84e88fc..0ee55e1 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -133,10 +133,15 @@ const buildLastmodMap = () => { const lastmodByRoute = buildLastmodMap(); +// Preview-build af udkast (PW_PREVIEW=1): hele sitet under /udkast, uden sitemap +// og service worker. Det serveres kun bag login og må aldrig ende i et index. +const PREVIEW = process.env.PW_PREVIEW === '1'; + export default defineConfig({ site: 'https://asger.xn--weirse-fya.dk', + ...(PREVIEW ? { base: '/udkast' } : {}), trailingSlash: 'ignore', - integrations: [ + integrations: PREVIEW ? [] : [ sitemap({ changefreq: 'weekly', priority: 0.7, diff --git a/scripts/verify-approvals.mjs b/scripts/verify-approvals.mjs new file mode 100644 index 0000000..a030a8f --- /dev/null +++ b/scripts/verify-approvals.mjs @@ -0,0 +1,65 @@ +// Spærre før build: intet blogindlæg går live uden at være godkendt. +// +// Hvert indlæg i src/content/blog skal bære et `godkendt:`-stempel i frontmatter. +// Stemplet er en HMAC-SHA256 over filens sti + indhold (uden selve stempel-linjen), +// lavet med en nøgle som kun godkendelsestjenesten og denne workflow kender. Et +// indlæg der er lagt direkte i repoet, eller ændret efter godkendelse, har derfor +// intet eller et forkert stempel, og så stopper buildet før noget bliver deployet. +// +// Udkast hører ikke hjemme her: repoet er offentligt, så `draft: true` ville stadig +// udgive teksten via git. Udkast ligger i et privat repo indtil de er godkendt. +// +// PUBLISH_KEY= node scripts/verify-approvals.mjs +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +const DIR = 'src/content/blog'; +const key = process.env.PUBLISH_KEY; +if (!key) { + console.error('PUBLISH_KEY mangler; kan ikke verificere godkendelser.'); + process.exit(1); +} + +// Skal matche godkendelsestjenesten byte for byte. +const canonical = (relPath, text) => { + const lines = text.replace(/\r\n/g, '\n').split('\n'); + if (lines[0] === '---') { + const end = lines.indexOf('---', 1); + if (end > 0) { + for (let i = end - 1; i > 0; i--) if (/^godkendt:/.test(lines[i])) lines.splice(i, 1); + } + } + return `${relPath}\n${lines.join('\n')}`; +}; + +const stampOf = (text) => { + const lines = text.replace(/\r\n/g, '\n').split('\n'); + const end = lines[0] === '---' ? lines.indexOf('---', 1) : -1; + if (end < 0) return { stamp: null, draft: false }; + const fm = lines.slice(1, end); + const g = fm.find((l) => /^godkendt:/.test(l)); + const draft = fm.some((l) => /^draft:\s*true\s*$/.test(l)); + return { stamp: g ? g.replace(/^godkendt:\s*/, '').replace(/^['"]|['"]$/g, '') : null, draft }; +}; + +const files = fs.readdirSync(DIR, { recursive: true }) + .filter((f) => f.endsWith('.md') || f.endsWith('.mdx')) + .map((f) => path.posix.join(DIR, f.split(path.sep).join('/'))); + +let bad = 0; +for (const rel of files) { + const text = fs.readFileSync(rel, 'utf8'); + const { stamp, draft } = stampOf(text); + if (draft) { console.error(`✗ ${rel}: udkast (draft: true) må ikke ligge i det offentlige repo`); bad++; continue; } + if (!stamp) { console.error(`✗ ${rel}: mangler godkendelse`); bad++; continue; } + const want = 'v1:' + crypto.createHmac('sha256', Buffer.from(key, 'hex')).update(canonical(rel, text)).digest('hex'); + const ok = stamp.length === want.length && crypto.timingSafeEqual(Buffer.from(stamp), Buffer.from(want)); + if (!ok) { console.error(`✗ ${rel}: godkendelsen passer ikke til indholdet (ændret efter godkendelse?)`); bad++; continue; } + console.log(`✓ ${rel}`); +} +if (bad) { + console.error(`\n${bad} indlæg er ikke godkendt. Intet bliver udgivet.`); + process.exit(1); +} +console.log(`\nAlle ${files.length} indlæg er godkendt.`); diff --git a/src/consts.ts b/src/consts.ts index c746a7c..aed3c9a 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -31,3 +31,6 @@ export const PROJECTS = { hide: [] as string[], pinned: ['ubl-tools', 'gtk-ahfail', 'pi3-smart-workspace'] as string[], }; + +// Sat når udkast bygges til preview (se astro.config.mjs). +export const PREVIEW = typeof process !== 'undefined' && process.env?.PW_PREVIEW === '1'; diff --git a/src/content.config.ts b/src/content.config.ts index c29250d..70ba381 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -11,6 +11,8 @@ const blog = defineCollection({ updated: z.coerce.date().optional(), tags: z.array(z.string()).default([]), draft: z.boolean().default(false), + // Godkendelsesstempel; tjekkes af scripts/verify-approvals.mjs før build. + godkendt: z.string().optional(), }), }); diff --git a/src/content/blog/ah-fail-skaermlaas-rust.md b/src/content/blog/ah-fail-skaermlaas-rust.md index 520f8a8..9772b9a 100644 --- a/src/content/blog/ah-fail-skaermlaas-rust.md +++ b/src/content/blog/ah-fail-skaermlaas-rust.md @@ -4,6 +4,7 @@ description: "Hvorfor jeg droppede ANSI C til fordel for Rust, og hvordan du ins date: 2026-06-11 tags: [rust, pam, gtklock, wayland, linux, macos] draft: false +godkendt: "v1:c8d80c56e01731b150a66ec9dcdfca911d932ea1721145fb79e3cbf46bc26046" --- Et af mine mere fjollede sideprojekter: **ah-fail**. Taster du dit kodeord forkert diff --git a/src/content/blog/authentik-oidc-faldgruber.md b/src/content/blog/authentik-oidc-faldgruber.md index 24f4108..464f0bb 100644 --- a/src/content/blog/authentik-oidc-faldgruber.md +++ b/src/content/blog/authentik-oidc-faldgruber.md @@ -4,6 +4,7 @@ description: "redirect_uri-mismatch, X-Forwarded-Proto, sub vs. email og de andr date: 2026-06-01 tags: [authentik, oidc, sso, selfhost] draft: false +godkendt: "v1:6c1d0bac31fd0547c7b0fc4e1c3dbb4ec8bf13d5960f33d8df90766c7700a1cd" --- Jeg kører **Authentik** som central identitetsudbyder i mit homelab, så jeg kan diff --git a/src/content/blog/authentik-reverse-proxy-forward-auth.md b/src/content/blog/authentik-reverse-proxy-forward-auth.md index 8f03fa8..9ab57bb 100644 --- a/src/content/blog/authentik-reverse-proxy-forward-auth.md +++ b/src/content/blog/authentik-reverse-proxy-forward-auth.md @@ -4,6 +4,7 @@ description: "Forward auth eller proxy provider? Og hvorfor store auth-headers g date: 2026-06-05 tags: [authentik, reverse-proxy, oidc, nginx] draft: false +godkendt: "v1:c0acd362adab5c144d08b0e2560513d3efa83c851632f4612dbcad2c9920a3bb" --- Når du sætter **Authentik** bag en reverse proxy, skal du tidligt træffe et valg: diff --git a/src/content/blog/er-mit-homelab-opdateret.md b/src/content/blog/er-mit-homelab-opdateret.md index e3db34e..2f16eb5 100644 --- a/src/content/blog/er-mit-homelab-opdateret.md +++ b/src/content/blog/er-mit-homelab-opdateret.md @@ -4,6 +4,7 @@ description: "Jeg byggede et lille overblik der selv kigger ind i alle mine cont date: 2026-06-13 tags: [proxmox, homelab, lxc, selfhost] draft: false +godkendt: "v1:8f1db814557e81e8015ac5f9173cbe414141c8373fcfc8149b485d006547ed47" --- Det startede som det altid gør.. med en lille irritation. Jeg ville tjekke om min diff --git a/src/content/blog/inject-javascript-npmplus-sub-filter.md b/src/content/blog/inject-javascript-npmplus-sub-filter.md index c7448e0..c3c31fc 100644 --- a/src/content/blog/inject-javascript-npmplus-sub-filter.md +++ b/src/content/blog/inject-javascript-npmplus-sub-filter.md @@ -4,6 +4,7 @@ description: "Sådan sprøjter du et script ind i HTML for hver app bag din reve date: 2026-06-08 tags: [npmplus, nginx, sub-filter, reverse-proxy, selfhost, matomo] draft: false +godkendt: "v1:ab462ce1615149e1a5b65e2b5f5507e97ce231340822408ffc8d5f15956a395a" --- Jeg ville have **Matomo-analytics** på tværs af alle mine selvhostede apps uden at diff --git a/src/content/blog/npm-til-npmplus.md b/src/content/blog/npm-til-npmplus.md index 3ffae29..91852cc 100644 --- a/src/content/blog/npm-til-npmplus.md +++ b/src/content/blog/npm-til-npmplus.md @@ -4,6 +4,7 @@ description: "Hvad der gik godt, hvad der brød, og hvorfor backup ikke er til f date: 2026-06-07 tags: [npmplus, nginx-proxy-manager, reverse-proxy, selfhost] draft: false +godkendt: "v1:5d850f0e2717f7928c31972a08f61f35eb7189adfe0e3c776cbd42318e39bfba" --- I årevis har min edge kørt på **Nginx Proxy Manager** (NPM). Det er en fremragende diff --git a/src/content/blog/proxmox-hjemmelab.md b/src/content/blog/proxmox-hjemmelab.md index ba9a4ae..2fbe4fe 100644 --- a/src/content/blog/proxmox-hjemmelab.md +++ b/src/content/blog/proxmox-hjemmelab.md @@ -4,6 +4,7 @@ description: "Et kig på opbygningen: reverse proxy, SSO, CI/CD og en masse cont date: 2026-06-09 tags: [proxmox, selfhost, homelab] draft: false +godkendt: "v1:b6deba4fbb95a8418cce1ef7b688f612ce5b4065d60bb5205cd6bf36da8e70e0" --- Mit hjemmelab kører på **Proxmox VE** med en blanding af LXC-containere og enkelte diff --git a/src/content/blog/proxmox-lxc-fejl.md b/src/content/blog/proxmox-lxc-fejl.md index f96e38a..dc825cf 100644 --- a/src/content/blog/proxmox-lxc-fejl.md +++ b/src/content/blog/proxmox-lxc-fejl.md @@ -4,6 +4,7 @@ description: "Unprivileged kontra privileged, uid-mapping til nobody, og hvorfor date: 2026-05-26 tags: [proxmox, lxc, homelab, selfhost] draft: false +godkendt: "v1:e9ae34841a773a0941e783eb3d16c4bc772cd923d13f0caf22575682400347a1" --- Næsten alt i mit homelab kører i **LXC-containere** på **Proxmox VE** frem for diff --git a/src/content/blog/velkommen.md b/src/content/blog/velkommen.md index f308c6b..6087f8c 100644 --- a/src/content/blog/velkommen.md +++ b/src/content/blog/velkommen.md @@ -4,6 +4,7 @@ description: "Hvad siden er, og hvad du kan forvente at finde her." date: 2026-06-10 tags: [meta, selfhost] draft: false +godkendt: "v1:1d29e33762bae4023bbc57d2870ab97c401aef100074da53c1434d99fff5702f" --- Velkommen. Det her er min nye personlige side.. bygget med Astro på Bun, hostet på diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index c952f80..8c1c4fa 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -5,7 +5,7 @@ import Header from '../components/Header.astro'; import Footer from '../components/Footer.astro'; import Palette from '../components/Palette.astro'; import CookieBanner from '../components/CookieBanner.astro'; -import { SITE } from '../consts'; +import { SITE, PREVIEW } from '../consts'; import '../styles/global.css'; interface Props { title?: string; @@ -56,13 +56,18 @@ const { title, description, image, type, publishedTime, modifiedTime, tags, json }); })(); - + - + {!PREVIEW && } + {PREVIEW && ( +
+ UDKAST, ikke udgivet. Tilbage til godkendelse +
+ )}
diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index a80bf06..16f0e59 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -1,7 +1,7 @@ --- import { getCollection, render } from 'astro:content'; import Base from '../../layouts/Base.astro'; -import { SITE } from '../../consts'; +import { SITE, PREVIEW } from '../../consts'; import { blogPosting, breadcrumb } from '../../seo'; export async function getStaticPaths() { @@ -57,6 +57,8 @@ const crumbs = breadcrumb([
+ {!PREVIEW && ( +

Kommentarer

@@ -72,6 +74,8 @@ const crumbs = breadcrumb([ } })(); + + )}