diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5008552..57a88d4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -106,9 +106,11 @@ jobs: # Application (deploy/auth.project.yml). Each with the numeric user it # runs as, read back from what was pushed rather than assumed # (deploy-kit spec/v1/20-resolved-deployment.md#the-images-lock). An - # image that names no user runs as root, 0, as both do today, which the - # shared resolve-image-lock step refuses, so this lock stays here. Each - # entry's version is its own release's (JorisJonkers-dev/workspace#140). + # image that names no user runs as root, and a root image is refused + # here: deploy-kit 0.31 renders its uid 0 beside runAsNonRoot, which the + # kubelet refuses at pod start, so a lock naming one would take sign-in + # down on the estate path. Each entry's version is its own release's + # (JorisJonkers-dev/workspace#140). env: GH_TOKEN: ${{ github.token }} API_IMAGE: ghcr.io/jorisjonkers-dev/auth-api/auth-api @@ -123,7 +125,7 @@ jobs: named="$(docker buildx imagetools inspect "$1" --format '{{ json .Image }}' \ | jq -r 'if has("linux/amd64") then .["linux/amd64"] else . end | .config.User // ""')" case "$named" in - ""|root|0) echo "0:0" ;; + ""|root|0|0:*) echo "::error::$1 runs as root; the estate path runs only a non-root image" >&2; return 1 ;; *[!0-9:]*) echo "::error::$1 runs as '${named}', not a numeric uid[:gid]" >&2; return 1 ;; *:*) echo "$named" ;; *) echo "${named}:${named}" ;; diff --git a/Dockerfile b/Dockerfile index 92112f4..a549528 100644 --- a/Dockerfile +++ b/Dockerfile @@ -92,4 +92,10 @@ COPY --from=build /app/api/build/libs/*.jar app.jar COPY --from=otel /otel-javaagent.jar otel-javaagent.jar COPY entrypoint.sh /app/entrypoint.sh EXPOSE 8081 +# A numeric user, so the image runs as non-root wherever it runs: the estate path's +# restricted posture sets runAsNonRoot and runs the uid the images lock reads from +# here, and a root image there is refused at pod start +# (deploy-kit spec/v1/20-resolved-deployment.md#the-images-lock). The JVM writes only +# /tmp, which the render mounts writable. +USER 65532:65532 ENTRYPOINT ["/app/entrypoint.sh", "-javaagent:otel-javaagent.jar", "-jar", "app.jar"] diff --git a/deploy/auth.project.yml b/deploy/auth.project.yml index c6c71d8..51229b7 100644 --- a/deploy/auth.project.yml +++ b/deploy/auth.project.yml @@ -3,14 +3,10 @@ # product (deploy-kit spec/v1/examples/auth/auth.project.yml). # # The Project is `auth`, so the namespace derives as `auth-system`, where both already run. -# It is still `legacy` (JorisJonkers-dev/estate platform.intent.yml): fleet-infra delivers it, -# and the estate composes this file without delivering anything for it. Its fragment exists so -# a Project on the estate path can sign in through auth and name it in `dependsOn` -# (deploy-kit spec/v1/60-setup.md#handing-over-one-project-at-a-time). The handover revisits -# every field here against the rendered diff. -# -# Each value below is read from the live objects. No env file: the old path still configures -# both Processes, and nothing on the estate path renders them yet. +# It moves from fleet-infra to the estate path in the three changes of deploy-kit +# spec/v1/60-setup.md#handing-over-one-project-at-a-time (#80). Each value below is read from +# the live objects, and each Process's environment from the old path's Deployment, in +# env//base.env beside this file. apiVersion: intent.jorisjonkers.dev/v1 kind: Project @@ -107,6 +103,10 @@ applications: access: read delivery: env rotation: { tolerates: restart } + # Granted on the derived role, auth-system-auth-api, but not yet read through it: + # auth-api also reads transit/keys/auth-api-jwt for its JWKS, which no transit + # operation derives, so it logs in as the old path's role until deploy-kit can + # declare that read (env/auth-api/base.env, VAULT_KUBERNETES_ROLE). - engine: transit key: auth-api-jwt operations: [sign] @@ -119,17 +119,18 @@ applications: runtime: static provides: - http: 80 + http: 8080 # the unprivileged nginx: no capability to bind a port below 1024 placement: memory: 192Mi cpu: 100m - writablePaths: [/var/cache/nginx, /var/run] + # The unprivileged nginx keeps its pid and temp files under /tmp. + writablePaths: [/tmp] probes: - readiness: { path: /, port: 80 } - liveness: { path: /, port: 80 } + readiness: { path: /, port: 8080 } + liveness: { path: /, port: 8080 } startupBudget: 30s diff --git a/deploy/env/auth-api/base.env b/deploy/env/auth-api/base.env new file mode 100644 index 0000000..1386d29 --- /dev/null +++ b/deploy/env/auth-api/base.env @@ -0,0 +1,92 @@ +# The Process scope of auth-api (deploy-kit spec/v1, chapter 10, Configuration). +# +# Every variable the old path's Deployment sets, carried over as it is today +# (fleet-infra cluster/flux/apps/stateless/auth-api/deployment.yaml), except: +# - the coordinates of what auth-api depends on and its own public address, +# which are placeholders now, so they follow the composition; +# - the values the Vault Agent Injector templated into /vault/secrets/auth-api.env, +# which are ${secret:...} placeholders now, synced by VSO into Secrets; +# - DEPLOYMENT_ENVIRONMENT, OTEL_SERVICE_NAME and OTEL_EXPORTER_OTLP_ENDPOINT, which +# the jvm Runtime Profile injects; +# - the PYROSCOPE_* variables: their agent came from an init container the render +# has no counterpart for, so nothing would read them. +SERVER_PORT=8081 +SPRING_PROFILES_ACTIVE=prod + +# The old path's java flags: ZGC, and a heap of 55% of the limit, which leaves room +# under it for the OpenTelemetry agent's native memory during the cold start. +JAVA_TOOL_OPTIONS=-XX:+UseZGC -XX:MaxRAMPercentage=55 + +# Spring Cloud Vault, `delivery: self`: the transit key the tokens are signed with, +# and the dynamic database login that takes precedence over DB_USER below. +# +# It logs in as the old path's role, auth-api, not as the role the render derives +# (auth-system-auth-api), on purpose. The token's JWKS is read from +# transit/keys/auth-api-jwt, and deploy-kit 0.31 can declare a transit operation +# (sign, verify, encrypt, decrypt, rotate) but not a read of the key, so the derived +# policy cannot cover what auth-api reads at startup. The old role still binds this +# ServiceAccount (auth-api in auth-system), and the old path's Vault bootstrap still +# writes it, with database/creds/auth-api beside the transit grants. +SPRING_CONFIG_IMPORT=vault:// +VAULT_ENABLED=true +VAULT_ADDR=http://vault.data-system.svc.cluster.local:8200 +VAULT_AUTHENTICATION=KUBERNETES +VAULT_KUBERNETES_ROLE=auth-api +VAULT_DB_ENABLED=true +AUTH_TRANSIT_ENABLED=true +AUTH_TRANSIT_KEY_NAME=auth-api-jwt + +DB_HOST=${dependency:platform-postgres.host} +DB_PORT=${dependency:platform-postgres.port} +DB_USER=${secret:secret/data/platform/postgres#auth.user} +DB_PASSWORD=${secret:secret/data/platform/postgres#auth.password} + +VALKEY_HOST=${dependency:platform-valkey.host} +VALKEY_PORT=${dependency:platform-valkey.port} + +# The static broker login: a dynamic one was revoked under a running pod once. +SPRING_RABBITMQ_HOST=${dependency:platform-rabbitmq.host} +SPRING_RABBITMQ_PORT=${dependency:platform-rabbitmq.port} +SPRING_RABBITMQ_USERNAME=${secret:secret/data/platform/rabbitmq#rabbitmq.user} +SPRING_RABBITMQ_PASSWORD=${secret:secret/data/platform/rabbitmq#rabbitmq.password} + +# The submission account stalwart provisions; its password has one source, +# secret/platform/mail. +MAIL_HOST=${dependency:stalwart.host} +MAIL_PORT=${dependency:stalwart.port} +MAIL_USERNAME=auth@jorisjonkers.dev +MAIL_PASSWORD=${secret:secret/data/platform/mail#account.password} + +AUTH_ISSUER=${exposure:auth.public#url} +AUTH_LOGIN_URL=${exposure:auth.public#url}/login +CONFIRMATION_URL=${exposure:auth.public#url}/confirm-email +PASSWORD_RESET_URL=${exposure:auth.public#url}/reset-password +SESSION_COOKIE_DOMAIN=jorisjonkers.dev +SESSION_COOKIE_SECURE=true + +# Every forward-auth host belongs here, whether or not it uses WebSockets today: +# a missing host breaks only a WebSocket handshake, with a 403 from Spring's CORS +# filter that logs nothing (the workspace's agent contract records the Overleaf case). +AUTH_CORS_ALLOWED_ORIGINS=https://jorisjonkers.dev,https://auth.jorisjonkers.dev,https://agents.jorisjonkers.dev,https://vault.jorisjonkers.dev,https://n8n.jorisjonkers.dev,https://grafana.jorisjonkers.dev,https://dashboard.jorisjonkers.dev,https://rabbitmq.jorisjonkers.dev,https://status.jorisjonkers.dev,https://overleaf.jorisjonkers.dev,https://notes.jorisjonkers.dev,https://hindsight.jorisjonkers.dev,https://hindsight-api.jorisjonkers.dev,https://hindsight-private.jorisjonkers.dev,https://karakeep.jorisjonkers.dev,https://tribelt.jorisjonkers.dev,https://grimoire.jorisjonkers.dev + +# Each OIDC client's secret, one key of secret/auth-api each. +AUTH_CLIENTS_ESTATE_DASHBOARD_SECRET=${secret:secret/data/auth-api#auth.clients.estate-dashboard.secret} +AUTH_CLIENTS_FINDABILITY_EXPLORER_SECRET=${secret:secret/data/auth-api#auth.clients.findability-explorer.secret} +AUTH_CLIENTS_GRAFANA_SECRET=${secret:secret/data/auth-api#auth.clients.grafana.secret} +AUTH_CLIENTS_GRIMOIRE_SECRET=${secret:secret/data/auth-api#auth.clients.grimoire.secret} +AUTH_CLIENTS_N8N_SECRET=${secret:secret/data/auth-api#auth.clients.n8n.secret} +AUTH_CLIENTS_OUTLINE_SECRET=${secret:secret/data/auth-api#auth.clients.outline.secret} +AUTH_CLIENTS_TRIBELT_SECRET=${secret:secret/data/auth-api#auth.clients.tribelt.secret} +AUTH_CLIENTS_VAULT_SECRET=${secret:secret/data/auth-api#auth.clients.vault.secret} + +# The trace exporter's tuning from the old path. +OTEL_LOGS_EXPORTER=none +OTEL_METRICS_EXPORTER=none +OTEL_TRACES_EXPORTER=otlp +OTEL_EXPORTER_OTLP_TIMEOUT=5000 +OTEL_BSP_SCHEDULE_DELAY=5000 +OTEL_BSP_MAX_QUEUE_SIZE=16384 +OTEL_BSP_MAX_EXPORT_BATCH_SIZE=2048 +# The profile's endpoint is the collector's otlp surface, 4317, which speaks gRPC; the +# agent's default protocol, http/protobuf, is the old path's port 4318. +OTEL_EXPORTER_OTLP_PROTOCOL=grpc diff --git a/deploy/env/auth-ui/base.env b/deploy/env/auth-ui/base.env new file mode 100644 index 0000000..9acbab4 --- /dev/null +++ b/deploy/env/auth-ui/base.env @@ -0,0 +1,5 @@ +# The Process scope of auth-ui (deploy-kit spec/v1, chapter 10, Configuration). +# +# Empty on purpose: auth-ui is nginx serving the built sign-in UI, and the old path's +# Deployment sets no variable for it. The UI reaches auth-api on the same host, under +# /api/, so it needs no address either.