docs: refresh README for the current layout; ASCII sweep on SECURITY.md
This commit is contained in:
@@ -46,3 +46,6 @@ sbom.json
|
||||
# Nothing under drafts/ may ever be committed; a draft becomes public only by
|
||||
# being moved into src/content/blog/ at publish time.
|
||||
drafts/
|
||||
|
||||
# Publish-workflow archives stay local for the same reason.
|
||||
published/
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
# bztmon-site
|
||||
|
||||
The source for **[www.bztmon.com](https://www.bztmon.com)** — Jonathon Wright's
|
||||
portfolio / résumé site. A fast, animated, security-hardened static site for a
|
||||
platform / infrastructure engineer.
|
||||
Source for [www.bztmon.com](https://www.bztmon.com) - portfolio and blog for a
|
||||
platform / infrastructure engineer. Static output, no server runtime, hardened
|
||||
by default.
|
||||
|
||||
> This repo is **public**. It lives on a self-hosted public Gitea (`git.bztmon.com`),
|
||||
> isolated from the private homelab GitOps. **Never commit secrets** — the static
|
||||
> site needs none.
|
||||
> This repo is public and lives on a self-hosted Gitea (`git.bztmon.com`),
|
||||
> isolated from the private GitOps repos. The static site needs no secrets;
|
||||
> none are committed.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Astro** (static output) + **TypeScript** + **Tailwind v4**
|
||||
- Zero JS by default; tiny islands for the theme toggle + scroll reveals
|
||||
- Content & config are data-driven (`src/data/`) — adding a project never touches a component
|
||||
- Astro (static output), TypeScript, Tailwind v4
|
||||
- Zero JS by default; small islands for the theme toggle and scroll reveals
|
||||
- Content and config are data-driven (`src/data/`, `src/content/`) - adding a
|
||||
post or project never touches a component
|
||||
|
||||
## Develop
|
||||
|
||||
@@ -20,72 +21,59 @@ platform / infrastructure engineer.
|
||||
npm install
|
||||
npm run dev # http://localhost:4321
|
||||
npm run check # astro check (types + diagnostics)
|
||||
npm run build # static build → dist/
|
||||
npm run build # static build -> dist/
|
||||
npm run preview # serve the build locally
|
||||
npm run gen:og # regenerate the social-preview image (public/og.png)
|
||||
```
|
||||
|
||||
## Project layout
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
data/ site.ts, socials.ts, skills.ts, projects.ts, experience.ts
|
||||
components/ Hero, Nav, ThemeToggle, ProjectCard, SkillGroup, ...
|
||||
content/blog/ posts (Markdown, zod-validated frontmatter)
|
||||
content/projects/ project dossiers
|
||||
data/ site.ts, series.ts, skills.ts, projects.ts, experience.ts
|
||||
components/ Hero, Nav, PostList, ProjectCard, ...
|
||||
layouts/ Layout.astro (SEO/OG, theme bootstrap)
|
||||
pages/ index.astro, projects/, 404.astro
|
||||
pages/ index, blog/, projects/, 404
|
||||
styles/ tokens.css (theme), global.css
|
||||
lib/ build-time helpers (cv detection)
|
||||
scripts/ gen-og.mjs, build-image.sh
|
||||
scripts/ gen-og.mjs, build-image.sh, new-post.mjs
|
||||
public/ heroes (blog/), diagrams (diagrams/), og.png, cv
|
||||
nginx/ default.conf (security headers, caching) baked into the image
|
||||
Dockerfile Debian build stage → nginx-unprivileged runtime
|
||||
Dockerfile Debian build stage -> nginx-unprivileged runtime
|
||||
```
|
||||
|
||||
## Content TODOs (Jonathon)
|
||||
## Posts
|
||||
|
||||
- Drop a real CV at `public/cv.pdf` — the **Download CV** button appears automatically.
|
||||
- Fill the `TODO(Jonathon)` markers in `src/data/experience.ts`, `projects.ts`, `socials.ts`
|
||||
(employer names, dates, GitHub/LinkedIn handles).
|
||||
|
||||
## Publishing a post
|
||||
|
||||
A post is just a Markdown file in `src/content/blog/`. Write one by hand, or generate
|
||||
a schema-valid one with the publish helper (this is the seam an IaC/CI step calls):
|
||||
|
||||
```bash
|
||||
# from flags
|
||||
node scripts/new-post.mjs --title "My post" --summary "One line" \
|
||||
--tags "kubernetes,gpu" [--draft] [--bodyFile notes.md]
|
||||
|
||||
# from a JSON event (e.g. an Ansible/AWX deploy summary)
|
||||
echo '{"title":"...","summary":"...","tags":["x"],"body":"## Hi\n..."}' \
|
||||
| node scripts/new-post.mjs --stdin
|
||||
```
|
||||
|
||||
Commit the file to `main` → CI rebuilds and ships. A malformed post **fails the build**
|
||||
(frontmatter is zod-validated), so a bad pipeline event never reaches production.
|
||||
A post is a Markdown file in `src/content/blog/` with zod-validated
|
||||
frontmatter; a malformed post fails the build, so a bad file never reaches
|
||||
production. `scripts/new-post.mjs` scaffolds one from flags or a JSON event on
|
||||
stdin - the seam a CI step can call. Each post belongs to one numbered series
|
||||
(`src/data/series.ts`), which drives the badge on the index cards.
|
||||
|
||||
## CI/CD
|
||||
|
||||
`.gitea/workflows/deploy.yml` runs on a self-hosted runner (a dedicated unprivileged
|
||||
user on the bastion):
|
||||
`.gitea/workflows/deploy.yml` runs on a self-hosted runner (a dedicated
|
||||
unprivileged user on a utility host):
|
||||
|
||||
```
|
||||
npm ci → astro check → audit-ci (high/critical gate) → build → scan dist →
|
||||
SBOM (CycloneDX) → buildah build+push → open a digest-bump PR to home-ops
|
||||
npm ci -> astro check -> audit-ci (high/critical gate) -> build -> scan dist ->
|
||||
SBOM (CycloneDX) -> buildah build+push -> open a digest-bump PR to home-ops
|
||||
```
|
||||
|
||||
The PR is **never auto-merged** — `home-ops` `main` is branch-protected; merging it is
|
||||
what triggers the ArgoCD rollout. The runner holds only least-privilege creds (a
|
||||
`home-ops`-scoped deploy key + a PR token + a registry push token).
|
||||
The PR is never auto-merged - the deploy repo's `main` is branch-protected, and
|
||||
merging the PR is what triggers the ArgoCD rollout. The runner holds
|
||||
least-privilege credentials only: a scoped deploy key, a PR token, and a
|
||||
registry push token.
|
||||
|
||||
- `npm run scan` — build-time gate: no secrets, no inline scripts, no third-party origins.
|
||||
- `.audit-ci.json` — fails on high/critical advisories. One allowlisted: `GHSA-gv7w-rqvm-qjhr`
|
||||
(esbuild install-integrity; build-time only, mitigated by the committed lockfile + trusted registry).
|
||||
- `renovate.json` — keeps npm deps and the digest-pinned base images current.
|
||||
- `npm run scan` - build-time gate: no secrets, no inline scripts, no
|
||||
third-party origins in `dist/`
|
||||
- `.audit-ci.json` - fails the build on high/critical advisories
|
||||
- `renovate.json` - keeps npm deps and the digest-pinned base images current
|
||||
|
||||
## Deploy
|
||||
|
||||
Built into a container image, served by nginx-unprivileged on a homelab Kubernetes
|
||||
cluster, exposed via Cloudflare Tunnel. The image is pinned by digest in the private
|
||||
`home-ops` repo and rolled out by ArgoCD. Manual/bootstrap build: `scripts/build-image.sh push`.
|
||||
See `SECURITY.md` for the full security posture.
|
||||
Built into a container image, served by nginx-unprivileged on Kubernetes,
|
||||
exposed outbound-only via Cloudflare Tunnel. The image is pinned by digest in
|
||||
the private deploy repo and rolled out by ArgoCD. Manual bootstrap build:
|
||||
`scripts/build-image.sh push`. Full posture in `SECURITY.md`.
|
||||
|
||||
+9
-9
@@ -5,17 +5,17 @@ as acceptance criteria, not polish.
|
||||
|
||||
## Attack surface
|
||||
|
||||
- **Static output** — no server runtime, no database, no user input, no forms.
|
||||
- **Static output** - no server runtime, no database, no user input, no forms.
|
||||
- Served by `nginx-unprivileged` (uid 101, read-only root filesystem, all Linux
|
||||
capabilities dropped, no service-account token) on Kubernetes.
|
||||
- Exposed **outbound-only via a Cloudflare Tunnel** — no open inbound ports, a single
|
||||
- Exposed **outbound-only via a Cloudflare Tunnel** - no open inbound ports, a single
|
||||
public hostname, no catch-all.
|
||||
|
||||
## Headers
|
||||
|
||||
Split by where they belong:
|
||||
|
||||
**Origin (nginx, ships in the image — `nginx/security-headers.conf`)**
|
||||
**Origin (nginx, ships in the image - `nginx/security-headers.conf`)**
|
||||
|
||||
| Header | Value |
|
||||
|---|---|
|
||||
@@ -23,11 +23,11 @@ Split by where they belong:
|
||||
| X-Content-Type-Options | `nosniff` |
|
||||
| X-Frame-Options | `DENY` |
|
||||
| Referrer-Policy | `strict-origin-when-cross-origin` |
|
||||
| Permissions-Policy | camera/mic/geo/payment/usb… all denied |
|
||||
| Permissions-Policy | camera/mic/geo/payment/usb... all denied |
|
||||
| Cross-Origin-Opener-Policy | `same-origin` |
|
||||
| Cross-Origin-Resource-Policy | `same-origin` |
|
||||
|
||||
**Edge (Cloudflare dashboard)** — HSTS, HTTP→HTTPS redirect, SSL Full, Bot Fight,
|
||||
**Edge (Cloudflare dashboard)** - HSTS, HTTP->HTTPS redirect, SSL Full, Bot Fight,
|
||||
rate-limiting. HSTS ships **without** `includeSubDomains`/`preload` initially because
|
||||
`*.bztmon.com` resolves to the WAN and the subdomain form would brick non-public hosts.
|
||||
|
||||
@@ -36,7 +36,7 @@ rate-limiting. HSTS ships **without** `includeSubDomains`/`preload` initially be
|
||||
- **`script-src 'self'` with zero inline scripts.** All JS (pre-paint theme, toggle,
|
||||
scroll reveal) lives in one external `/site.js`. No `unsafe-inline`, no `unsafe-eval`,
|
||||
no hashes to maintain.
|
||||
- **`style-src` allows `'unsafe-inline'`** — the one conscious exception. Shiki's
|
||||
- **`style-src` allows `'unsafe-inline'`** - the one conscious exception. Shiki's
|
||||
dual-theme syntax highlighting emits per-token CSS custom-properties as inline `style`
|
||||
attributes; hashing them is impractical (they vary per page). The security-critical
|
||||
directive (`script-src`) stays strict. Everything else is self-hosted: fonts are system
|
||||
@@ -55,9 +55,9 @@ Runs in CI before the image is built.
|
||||
|
||||
## Targets (verified post-deploy)
|
||||
|
||||
- securityheaders.com → A+
|
||||
- Mozilla Observatory → A/A+ (small deduction expected for `style-src 'unsafe-inline'`)
|
||||
- Lighthouse ≥ 95 across Performance / Accessibility / Best Practices / SEO
|
||||
- securityheaders.com -> A+
|
||||
- Mozilla Observatory -> A/A+ (small deduction expected for `style-src 'unsafe-inline'`)
|
||||
- Lighthouse >= 95 across Performance / Accessibility / Best Practices / SEO
|
||||
- CSP: zero console violations in a real browser (incl. code blocks + diagrams)
|
||||
|
||||
## Reporting
|
||||
|
||||
Reference in New Issue
Block a user