Skip to content

v0.9.8 — Security hardening release

Choose a tag to compare

@pandeyn pandeyn released this 03 May 23:33
· 112 commits to main since this 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 an HttpOnly (and Secure when HTTPS) cookie before redirect, constant-time compared on callback via subtle::ConstantTimeEq. Token cookies gain Secure when the app serves over HTTPS.
  • Production CORS. APP_ENV=production reads CORS_ALLOWED_ORIGINS from the environment, restricts methods to GET, POST, PUT, DELETE, OPTIONS and headers to AUTHORIZATION, 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: false and capabilities.drop: [ALL]. Non-SQLite paths add a writable /tmp emptyDir so readOnlyRootFilesystem: true doesn't crash on first temp-file write.
  • K8s and Helm secrets. Static app-secret.yml ships empty stringData with three documented operator approaches (inline edit / kubectl create secret / Helm). The Helm chart uses a lookup-or-generate pattern for both JWT_SECRET and DB_PASSWORD that creates random values on first install and preserves them across helm upgrade so 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-production form and empty strings.
  • Database passwords stay in the Secret. Both static K8s manifests and the Helm chart now assemble DATABASE_URL in the Deployment env using K8s $(VAR) substitution against the Secret-imported DB_PASSWORD / POSTGRES_PASSWORD / MYSQL_ROOT_PASSWORD. Plaintext credentials no longer live in any ConfigMap.
  • CI hardening. Third-party actions in .github/workflows/samples.yml are pinned to commit SHAs; a new .github/dependabot.yml keeps them current; a shellcheck step lints the generated docker-entrypoint.sh on every sample-matrix run.
  • npm audit clean. Zero vulnerabilities via npm audit fix, npm overrides on 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-app

JWT_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:

  1. Edit the manifest: uncomment the example block and supply real values inline.
  2. kubectl create: skip applying the manifest and run kubectl create secret generic <app>-secret --from-literal=JWT_SECRET="$(openssl rand -hex 32)" --from-literal=POSTGRES_PASSWORD="$(openssl rand -base64 24)".
  3. Helm: helm install ./helm/<app> auto-generates both JWT_SECRET and DB_PASSWORD on first install and preserves them across upgrades. See README-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/admin default seed users (couples with a password-policy story)
  • /api/users authorization 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)
  • HttpOnly flag on access-token / id-token cookies (requires a SPA refactor since the SPA currently reads the token via document.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 new entrypoint.spec.js (7 cases) that exercises the rendered docker-entrypoint.sh via real bash invocations.
  • 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.