feat: add strapi template + hardening + trust reconcile (v1.1.0)#56
Conversation
- Validate BASE_REVISION in check-gpg.sh, check-ssh.sh and dockerfile.sh (reject shell metacharacters and '..' path traversal) before it is interpolated into any fetch URL or any destructive setup runs. curl normalizes '../', so an unvalidated value could repoint fetches at an arbitrary repo/path. - dockerfile.sh: reject newlines/CR in user-supplied -c/-f/-b/-r/--run values, closing a Dockerfile directive-injection vector; reject -f values with more than one ';' (the credential guard and the COPY generator parsed them differently). - check-ssh.sh: fail closed if any ssh-allowed-signers line cannot be parsed into a fingerprint, instead of silently building a narrower allowlist. - Dockerfile.template: pre-create /home/<runner_user> before chown so the nginx template builds (nginx:stable-alpine ships no /home/nginx). nginx still runs non-root on :80, which requires the host to allow unprivileged low ports (net.ipv4.ip_unprivileged_port_start=0); documented inline. - generate-yarn-npm.sh: add an explicit warning that it writes a plaintext token for ephemeral runners only and must not be used inside a docker build layer (use dockerfile.sh's BuildKit secret mount instead). - CI: pin ludeeus/action-shellcheck to a commit SHA instead of @master; add regression tests for injection / ambiguous-path / BASE_REVISION rejection. - docker-compose.yaml: drop the obsolete top-level version key. - README.md: document the next template and correct the default command to ["npm", "start"]. Refresh checksum.sha256 for the changed scripts.
Two low-severity defense-in-depth follow-ups from the audit: - check-gpg.sh / check-ssh.sh: reject an empty signer key id / fingerprint before the `grep -Fxq` allowlist membership check. `grep -Fxq ""` matches a blank line, so an empty value combined with a stray blank line in the allowlist could otherwise pass. Not reachable today (the SIG=="G" gate implies a real key) but cheap to close. - dockerfile.sh: write the multi-line .yarnrc.yml in the generated build RUN with `printf` instead of `echo "...\n..."`. POSIX printf interprets `\n` in every shell, so the generated Dockerfile no longer depends on the builder's /bin/sh being dash. Verified the writes produce identical valid YAML under both sh and bash; the build still succeeds and the token still does not persist in any builder layer. Refreshed checksum.sha256 for the three changed scripts.
Add a `strapi` template to dockerfile.sh for building Strapi headless CMS apps, modeled on the existing node template. - Builder: orochinetwork/ubuntu:node (corepack enabled for Yarn 4 berry). - Runner DEFAULT: node:22-trixie-slim. glibc is required — Strapi's native sharp/libvips are built against glibc and break at runtime on Alpine/musl. A -r/RUNNER_IMAGE override is still honored. - NODE_ENV=production, EXPOSE 1337, CMD ["npm","run","start"]. - Default runtime copy set (overridable via -f): config src database public types dist .strapi tsconfig.json package.json node_modules favicon.png. tsconfig.json is load-bearing (Strapi reads outDir to locate dist/). - scripts/build-prod-strapi.sh: immutable install + yarn build (strapi build: admin panel + server -> dist). Intentionally does not write src/version.ts. - Register strapi in SUPPORTED_TEMPLATES; cover the new script in checksum.sha256; document it in DOCKERFILE.md/README.md/CHANGELOG.md; and add it to the CI dry-run smoke matrix with a runtime-contract assertion.
The SSH signer roster had 4 users while the GPG allowlist (gpg-list.asc / gpg/*.asc) holds 13 keys. Expand GITHUB_USERS so the SSH side mirrors the GPG side for active contributors. Every name added below already holds a GPG key in the trust anchor AND is an active orochi-network/dev-off contributor: alothanhh, BaoNinh2808, brianw3b, CaoHoaiTan, harris1111, hungnguyen18, ngotrongphuc, nguyendinhthang3101, SangTran-127, ThanhNguyen03, wonrax No one was removed. BaoNinh2808 and brianw3b currently publish no SSH keys on GitHub, so the generator skips them with a warning until they upload one; they are kept in the list so the rosters stay in parity. The gpg/Github.asc entry is the GitHub web-flow signing bot and is intentionally not an SSH signer. Trust-list additions are a security boundary and require human review before merge.
There was a problem hiding this comment.
All nodejs build will be the same? why do we need this file?
There was a problem hiding this comment.
Confirmed — a Strapi build is just the node build, so the separate script is gone. Removed build-prod-strapi.sh; the strapi template now reuses the shared build-prod-node.sh via a BUILD_SCRIPT_TEMPLATE indirection in dockerfile.sh (defaults to the template name, set to node for the strapi case). node/next/nginx are untouched.
Why none of the differences justified a separate file:
corepack enablewas redundant — the strapi template already emitsRUN corepack enableas its own builder layer, so corepack is active before the build script runs.--immutablevs--frozen-lockfile—--frozen-lockfileworks on Yarn 4 (it is a documented backward-compat alias for--immutable; emits aYN0050deprecation notice and proceeds). Same effect.- Skipping
src/version.tswas actually an inconsistency — every other node-family consumer gets it, and the write is already guarded by[ -d src/ ]. Strapi now gets it too.
The genuinely Strapi-specific bits stay in dockerfile.sh: glibc node:22-trixie-slim runner, the corepack builder layer, the default copy set, and NODE_ENV=production / EXPOSE 1337 / CMD ["npm","run","start"]. Dry-run confirms the generated Dockerfile now fetches build-prod-node.sh with all of those intact. Fixed in 046bdb1.
A Strapi build is a node build (frozen-lockfile install + `yarn build`, which runs `strapi build`), so the dedicated scripts/build-prod-strapi.sh was redundant: - corepack was enabled twice: dockerfile.sh already emits a dedicated `RUN corepack enable` builder layer for the strapi template, so the build script did not need to. - `yarn install --frozen-lockfile` (build-prod-node.sh) is a backward-compat alias for `--immutable` on Yarn 4 — same behavior, only a cosmetic YN0050 notice — so the `--immutable` difference was not load-bearing. - the `src/version.ts` write in build-prod-node.sh is already guarded by an `if [ -d src ]` check and is a consistent feature every node-family consumer gets; skipping it for strapi was an unnecessary inconsistency. Remove scripts/build-prod-strapi.sh and point the strapi template at the shared build-prod-node.sh via a new BUILD_SCRIPT_TEMPLATE indirection (defaults to the template name; strapi sets it to `node`). node/next/nginx behavior is unchanged. The Strapi-specific bits (glibc runner node:22-trixie-slim, corepack builder layer, default copy set, NODE_ENV/EXPOSE/CMD) stay in the dockerfile.sh case block. Update the CI smoke matrix to assert the strapi Dockerfile fetches build-prod-node.sh and references no build-prod-strapi.sh; refresh checksum.sha256 (now 11 files); update DOCKERFILE.md and CHANGELOG.md.
A multi-agent audit of the repo surfaced a set of correctness/security issues across the trust-verification scripts, the Dockerfile generator, and repo config. This PR fixes them, and additionally adds a first-class
strapitemplate plus a trust-roster reconciliation. Each change was verified locally (shellcheck,bash -n, the CI jobs, checksum freshness, anddockerfile.sh --dry-run).New in this revision (Phase 2)
1. First-class
strapitemplate (main deliverable — unblockswebsite-strapi)Adds a
strapitemplate todockerfile.sh(modeled on thenodetemplate) plusscripts/build-prod-strapi.sh:orochinetwork/ubuntu:node, withcorepack enable(Strapi uses Yarn 4 berry; the base image only ships Yarn 1).node:22-trixie-slim— glibc is required, because Strapi's nativesharp/libvipsare built against glibc and break at runtime on Alpine/musl. A-r/RUNNER_IMAGEoverride is still honored (keep it glibc).ENV NODE_ENV=production,EXPOSE 1337,CMD ["npm", "run", "start"].-f):config src database public types dist .strapi tsconfig.json package.json node_modules favicon.png.tsconfig.jsonis load-bearing — Strapi reads itsoutDirto locatedist/.scripts/build-prod-strapi.shrunsyarn install --immutable && yarn build(=strapi build: admin panel + server →dist). It intentionally does not writesrc/version.ts(Strapi owns itssrc/tree and type generation).SUPPORTED_TEMPLATES; covered bychecksum.sha256; documented inDOCKERFILE.md/README.md/CHANGELOG.md; and added to the CI dry-run smoke matrix with an assertion that the template keeps its glibc-runner / corepack /NODE_ENV=production/EXPOSE 1337/ CMD contract (and that-rstill overrides the default).2. Removed a stale, leaky root
DockerfileAn untracked root
Dockerfilelingered from before the secret-mount hardening — it wrote.npmrc/.yarnrc.ymlacross multipleRUNs (credentials persisted in builder layers) and referenced a non-existentbuild-prod-.sh. It was never git-tracked; removed the local artifact so it can't confuse anyone.3. Trust-roster reconciliation (security-sensitive — requires human review)
generate-ssh-allowed-signers.sh'sGITHUB_USERShad 4 entries while the GPG allowlist (gpg-list.asc/gpg/*.asc) holds 13 keys. Expanded the SSH side to mirror the GPG side for active contributors. No one was removed.Each username added below already holds a GPG key in the trust anchor and is an active
orochi-network/dev-offcontributor:alothanhhBaoNinh2808brianw3bCaoHoaiTanharris1111hungnguyen18ngotrongphucnguyendinhthang3101SangTran-127ThanhNguyen03wonraxNotes:
gpg/Github.ascis the GitHub web-flow signing bot and is intentionally not an SSH signer.brng1151,bao-ninh-orochi,chirojrwere already SSH signers without a matchinggpg/*.ascfile; left untouched (the drift report flags them as a NOTE only).Existing hardening (carried from the original PR)
Security / correctness
BASE_REVISIONvalidation (check-gpg.sh,check-ssh.sh,dockerfile.sh): the value is interpolated intocurlURLs (and the scripts run viacurl | bash). It is now validated before any fetch URL is built or any destructive setup runs — only a commit SHA, tag, or branch name is accepted; shell metacharacters and..are rejected. (curl normalizes../, so an unvalidated value could repoint fetches at an arbitrary repo/path.) Previouslycheck-gpg.sheven wiped/recreated~/.gnupgbefore looking at the value.dockerfile.sh): user values for-c/-f/-b/-r/--runare rejected if they contain a newline/CR. Without this,-f $'build\nRUN …'injected an extraRUNdirective into the generated Dockerfile.-fparse (dockerfile.sh): a-fvalue with more than one;is rejected — the credential guard readdstas the last field while the COPY generator read it as the second, so they disagreed on what was being copied.check-ssh.shfails closed: if anyssh-allowed-signersline cannot be parsed into a fingerprint, the script now errors instead of silently building a narrower allowlist.--mount=type=secretbuildRUN; enforced by thebuilder-secret-no-leakCI job.generate-yarn-npm.sh: now carries an explicit warning that it writes a plaintext token for ephemeral runners only and must not be used inside adocker buildlayer — image builds usedockerfile.sh's BuildKit secret mount.ludeeus/action-shellcheckpinned to a commit SHA (00cae50, tag 2.0.0) instead of@master; added regression tests for the injection / ambiguous-path /BASE_REVISIONrejections above.Bug fix
Dockerfile.template):chown -R nginx /home/nginxfailed becausenginx:stable-alpineships no/home/nginx. The runner stage now pre-creates/home/<runner_user>before chowning.Cleanup
docker-compose.yaml: removed the obsolete top-levelversionkey (ignored by Compose v2).README.md: documents thenexttemplate and corrects the default command to["npm", "start"].checksum.sha256for the changed scripts; updatedCHANGELOG.md/SECURITY.md.A note on the nginx port
The audit flagged the nginx runner as "would crash binding :80 because it runs non-root." That is environment-dependent, not a blanket Docker rule: it depends on the host's
net.ipv4.ip_unprivileged_port_start. On Docker Desktop (threshold0) a non-root container binds:80fine; on a stock-kernel Linux host (threshold1024) it fails. Per maintainer decision we keep:80and document the sysctl assumption inline; switching to8080is a one-line change if a stock-kernel host is ever a target.Verification
bash -nclean ondockerfile.sh,scripts/build-prod-strapi.sh,generate-ssh-allowed-signers.sh,generate-checksums.sh.shellcheck -e SC1091clean on the changed scripts.checksum.sha256regenerated and fresh (CI-equivalent diff is empty);sha256sum -c --strictpasses for all 12 covered files (now includingscripts/build-prod-strapi.sh)../dockerfile.sh -t strapi --dry-runproduces the expected builder/runner/corepack/NODE_ENV/EXPOSE 1337/CMD/copy-set output;--dry-runwrites nothing to disk.