add CSP-clean static build, hardened nginx config, and container image
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
*.log
|
||||
assets/work/
|
||||
dist/
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Static teaching site: bash assembles dist/ from the split source, then a pinned
|
||||
# nginx-unprivileged serves it. No Node build — build.sh is plain bash + coreutils.
|
||||
|
||||
# ---- build stage ----------------------------------------------------------
|
||||
# Reusing the digest the site image already pins: it's a bookworm-slim base that is
|
||||
# verified and cached in the zot pull-through mirror, so this layer costs nothing
|
||||
# extra. Only bash/sed/find/install are used here — node itself is incidental.
|
||||
FROM node:22-bookworm-slim@sha256:d9f850096136edbc402debdd8729579a288aac64574ada0ff4db26b6ae58b0b2 AS build
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN bash build.sh
|
||||
|
||||
# ---- runtime stage --------------------------------------------------------
|
||||
# Same vetted digest as the site. Renovate keeps it current.
|
||||
FROM ghcr.io/nginx/nginx-unprivileged:1.28.0-alpine@sha256:c97ff0bf7cbae369953c6da1232ec14ad9f971d66360c5698db0856a4cd657a0
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY nginx/security-headers.conf /etc/nginx/security-headers.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 8080
|
||||
@@ -4,7 +4,8 @@ Interactive scroll-driven teardown of Kubernetes / OpenShift / Docker, in the es
|
||||
Bat-Computer visual world (void `#070b14`, cyan `#3fbaf5` → magenta `#e879f9`).
|
||||
|
||||
- **v2** (tag `v2`) — CSS `preserve-3d` diagrams. Parked: eyeballed px positions, misaligned at some widths.
|
||||
- **v3** (in progress) — Nano Banana 2 exploded hero shots, hand-sliced into `clip-path` parts and
|
||||
- **v3** (in progress) — ships PUBLIC at **learn.bztmon.com**.
|
||||
- **v3 build** — Nano Banana 2 exploded hero shots, hand-sliced into `clip-path` parts and
|
||||
scrubbed collapsed → hero with GSAP. Spec: `homelab/specs/SPEC-exploded-cluster-v3.md`.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assemble dist/ — the CSP-clean static build served at learn.bztmon.com.
|
||||
#
|
||||
# The artifact build inlines GSAP and the app JS because the artifact CSP blocks every
|
||||
# external request. nginx has no such constraint, so here they stay SEPARATE files:
|
||||
# `script-src 'self'` holds with no 'unsafe-inline' exception, and the two GSAP bundles
|
||||
# (116KB, unchanged between deploys) become independently cacheable.
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
trap 'printf "build failed at line %s\n" "$LINENO" >&2' ERR
|
||||
|
||||
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd -- "$here"
|
||||
|
||||
dist="$here/dist"
|
||||
src_markup="ec2-part1.html"
|
||||
src_app="ec2-part2.html"
|
||||
|
||||
for f in "$src_markup" "$src_app" gsap.min.js st.min.js; do
|
||||
[[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; }
|
||||
done
|
||||
|
||||
rm -rf -- "$dist"
|
||||
mkdir -p -- "$dist"
|
||||
|
||||
# --- app.js: strip the single <script> wrapper part2 carries -----------------
|
||||
# part2 is exactly one <script>…</script> block; served standalone it must be raw JS.
|
||||
sed -e '1{/^[[:space:]]*<script>[[:space:]]*$/d}' \
|
||||
-e '${/^[[:space:]]*<\/script>[[:space:]]*$/d}' \
|
||||
-- "$src_app" > "$dist/app.js"
|
||||
|
||||
if grep -qiE '</?script' -- "$dist/app.js"; then
|
||||
printf 'app.js still contains a script tag — the wrapper strip did not match\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- index.html: markup + external script refs -------------------------------
|
||||
{
|
||||
cat -- "$src_markup"
|
||||
cat <<'HTML'
|
||||
|
||||
<script src="/gsap.min.js"></script>
|
||||
<script src="/st.min.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
} > "$dist/index.html"
|
||||
|
||||
# No inline JS may survive into the served HTML, or the CSP silently kills the page.
|
||||
# Every <script> opening tag must carry a src=. (grep -E has no lookahead — list the
|
||||
# tags, then assert none of them lacks src.)
|
||||
if grep -oiE '<script[^>]*>' -- "$dist/index.html" | grep -qivE 'src='; then
|
||||
printf 'index.html contains an inline <script> — CSP script-src self would block it\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -m 0644 -- gsap.min.js st.min.js "$dist/"
|
||||
|
||||
# --- scene assets ------------------------------------------------------------
|
||||
if [[ -d assets/dist ]]; then
|
||||
mkdir -p -- "$dist/assets"
|
||||
find assets/dist -type f -print0 | while IFS= read -r -d '' f; do
|
||||
install -m 0644 -- "$f" "$dist/assets/$(basename -- "$f")"
|
||||
done
|
||||
fi
|
||||
|
||||
printf 'built %s\n' "$dist"
|
||||
find "$dist" -type f -printf ' %-22f %8s bytes\n' | sort
|
||||
@@ -0,0 +1,51 @@
|
||||
# nginx config for the static teaching site, baked into the image.
|
||||
# Base: nginxinc/nginx-unprivileged (uid 101, listens on 8080).
|
||||
# Read-only rootfs in k8s: /tmp and /var/cache/nginx are emptyDir mounts.
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
server_tokens off;
|
||||
|
||||
# Behind the Cloudflare Tunnel on :443 while nginx listens on :8080 — without these,
|
||||
# nginx emits ABSOLUTE redirects to http://host:8080/... and breaks every link.
|
||||
absolute_redirect off;
|
||||
port_in_redirect off;
|
||||
|
||||
include /etc/nginx/security-headers.conf;
|
||||
|
||||
# ---- Caching -------------------------------------------------------------
|
||||
# Scene images are content-hashed at build time → safe to cache hard.
|
||||
location /assets/ {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable" always;
|
||||
}
|
||||
|
||||
# Scripts are NOT fingerprinted yet — revalidate so a deploy actually propagates.
|
||||
location ~* \.js$ {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
location ~* \.html$ {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
# ---- Routing -------------------------------------------------------------
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
}
|
||||
|
||||
# ---- Compression ---------------------------------------------------------
|
||||
# WebP/PNG are already compressed — gzipping them burns CPU for nothing.
|
||||
gzip on;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_vary on;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Shared security headers. `include`d in the server block AND in every location that
|
||||
# sets its own add_header — a location-level add_header REPLACES inherited headers
|
||||
# rather than merging them (the classic nginx footgun).
|
||||
#
|
||||
# HSTS is host-scoped: NO includeSubDomains / preload, because the *.bztmon.com
|
||||
# wildcard points elsewhere and forcing HTTPS estate-wide from here would be reckless.
|
||||
# HTTP->HTTPS upgrade is Cloudflare's job.
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
|
||||
# script-src 'self' with NO 'unsafe-inline'. build.sh externalises every script and
|
||||
# hard-fails if an inline <script> reaches dist/, so this holds without an exception.
|
||||
# style-src needs 'unsafe-inline': GSAP animates by writing inline style attributes.
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; worker-src 'self'; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()" always;
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
Reference in New Issue
Block a user