v0.9.8 — Security hardening release
v0.9.8 — Security hardening release
A security-focused release that hardens the templates the blueprint emits. Existing 0.9.7-generated apps are untouched on disk; this release affects projects you regenerate with v0.9.8.
Highlights
- JWT_SECRET handling, end-to-end. Scaffold-time CSPRNG hex (replaces the old timestamp-based default), runtime random per-container generation in a new
docker-entrypoint.sh, and a Rust startup check (server/src/config/sentinels.rs) that refuses to start when the value matches a known-default sentinel. The same denylist is shared by entrypoint, vault-init scripts, and the Rust binary. - OAuth2 CSRF state validation. State parameter generated from
OsRng, stored in anHttpOnly(andSecurewhen HTTPS) cookie before redirect, constant-time compared on callback viasubtle::ConstantTimeEq. Token cookies gainSecurewhen the app serves over HTTPS. - Production CORS.
APP_ENV=productionreadsCORS_ALLOWED_ORIGINSfrom the environment, restricts methods toGET, POST, PUT, DELETE, OPTIONSand headers toAUTHORIZATION, CONTENT_TYPE, and refuses to start when the variable is missing. Development stays permissive so local frontends work without operator config. - Container privilege. Both Dockerfile and K8s Deployment run as non-root UID 1001 with
allowPrivilegeEscalation: falseandcapabilities.drop: [ALL]. Non-SQLite paths add a writable/tmpemptyDirsoreadOnlyRootFilesystem: truedoesn't crash on first temp-file write. - K8s and Helm secrets. Static
app-secret.ymlships emptystringDatawith three documented operator approaches (inline edit /kubectl create secret/ Helm). The Helm chart uses a lookup-or-generate pattern for bothJWT_SECRETandDB_PASSWORDthat creates random values on first install and preserves them acrosshelm upgradeso existing tokens and the data volume stay readable. - No more sentinel literals. Five places that used to ship literal default secrets (dev compose, vault-init scripts on docker and K8s, consul ConfigMap on K8s and Helm) now either forward operator-supplied values, generate random ones, or omit the seed entirely. The denylist also catches the bare
change-me-in-productionform and empty strings. - Database passwords stay in the Secret. Both static K8s manifests and the Helm chart now assemble
DATABASE_URLin the Deployment env using K8s$(VAR)substitution against the Secret-importedDB_PASSWORD/POSTGRES_PASSWORD/MYSQL_ROOT_PASSWORD. Plaintext credentials no longer live in any ConfigMap. - CI hardening. Third-party actions in
.github/workflows/samples.ymlare pinned to commit SHAs; a new.github/dependabot.ymlkeeps them current; ashellcheckstep lints the generateddocker-entrypoint.shon every sample-matrix run. npm auditclean. Zero vulnerabilities vianpm audit fix, npmoverrideson transitive packages, and an eslint bump.
Migration
docker run
The runtime image no longer ships ENV DATABASE_URL=... or ENV MONGODB_URI=.... Supply them explicitly:
docker run \
-e DATABASE_URL=postgres://user:pass@host:5432/db \
-e JWT_SECRET="$(openssl rand -hex 32)" \
my-jhipster-rust-appJWT_SECRET is generated by the entrypoint on each container start when unset, so omitting it isn't fatal. But every container restart rotates the signing key, which invalidates existing tokens. Pin it explicitly when you care about session continuity.
kubectl apply -f k8s/
app-secret.yml ships with stringData: {} (empty). Pick one:
- Edit the manifest: uncomment the example block and supply real values inline.
kubectl create: skip applying the manifest and runkubectl create secret generic <app>-secret --from-literal=JWT_SECRET="$(openssl rand -hex 32)" --from-literal=POSTGRES_PASSWORD="$(openssl rand -base64 24)".- Helm:
helm install ./helm/<app>auto-generates bothJWT_SECRETandDB_PASSWORDon first install and preserves them across upgrades. SeeREADME-helm.md.
If you previously deployed via the unmodified static manifest and it worked, you were running with publicly-known credentials — that's what this release closes.
Helm install
helm install works out of the box with no --set flags: the chart generates random JWT_SECRET and DB_PASSWORD on first install and persists them in the in-cluster Secret. To pin specific values:
helm install <release> ./helm/<app> \
--set secrets.JWT_SECRET="$(openssl rand -hex 32)" \
--set secrets.DB_PASSWORD="$(openssl rand -base64 24)"Production CORS
APP_ENV=production now requires CORS_ALLOWED_ORIGINS to be set to a comma-separated list of origins. Empty or missing → the app refuses to start with a clear error. Development deployments (APP_ENV=development, the default) are unchanged.
Container UID
Containers run as UID 1001 (non-root). K8s Deployments enforce this with securityContext.runAsUser: 1001. If you have external volumes mounted with restrictive ownership, set fsGroup: 1001 on the pod's securityContext or pre-chown the volume contents.
Vault (dev compose)
The dev docker compose -f docker/vault.yml up flow now forwards JWT_SECRET from your project's .env to the vault-init container. If .env is absent or JWT_SECRET is unset, vault-init generates a fresh hex for the dev seed.
Roadmap (intentionally deferred to the next release)
A few items from the audit were scoped out of v0.9.8 because they require coordinated changes across the SPA frontend or upstream JHipster behavior. Operators should know they exist:
admin/admindefault seed users (couples with a password-policy story)/api/usersauthorization divergence from JHipster Spring Boot upstream (needs an explicit decision rather than a silent change)- Permissive CORS in dev mode (intentional — production is hardened in this release)
HttpOnlyflag on access-token / id-token cookies (requires a SPA refactor since the SPA currently reads the token viadocument.cookie)
These will land in the auth-hardening release.
What's tested
vitest: 440 generator specs (rust-server / kubernetes / helm / docker / cypress / client) plus a newentrypoint.spec.js(7 cases) that exercises the rendereddocker-entrypoint.shvia realbashinvocations.- End-to-end on a local kind cluster:
helm install→ postgres + app both Ready in ~17s, JWT auth roundtrip succeeds, forged tokens 401, sentinels rejected at startup. - Generation across SQLite / PostgreSQL / MySQL / MongoDB and the existing JHipster sample shapes (basic monolith, microservice with circuit breaker, gateway with circuit breaker).
Engine
generator-jhipster: ^9.0.0, node: ^22.18.0 || >= 24.11.0. No change vs 0.9.7.
Full notes: see RELEASE_NOTES.md for line-by-line file references.