| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- # Stateless multi-replica Deployment (task 46).
- #
- # - 3 replicas sharing one Postgres + one Redis: no replica holds state
- # another needs (tokens live in Redis under `{state}`-tagged keys).
- # - `serve` NEVER migrates; the separate `migrate-job.yaml` owns the schema.
- # Readiness (`/health/ready`) compares `_sqlx_migrations` against the
- # binary's embedded migrations, so new replicas stay out of traffic until
- # the Job has applied the schema (expand/contract keeps N-1 serving).
- # - Probes hit the PLAINTEXT pod-IP health listener (`health_addr`,
- # default `0.0.0.0:8081`); the public listener stays TLS-only.
- # - `terminationGracePeriodSeconds: 45` covers the server's 1s propagation
- # grace + 30s drain budget (`src/serve.rs`: PROPAGATION_GRACE, DRAIN_DEADLINE).
- # - Host hardening: non-root 65532, read-only rootfs, ALL capabilities
- # dropped, `allowPrivilegeEscalation: false`, `RuntimeDefault` seccomp, and
- # `automountServiceAccountToken: false` (the server never calls the
- # Kubernetes API). `networkpolicy.yaml` default-denies pod egress except DNS
- # + Postgres + Redis.
- # - Secret material is file-mounted, never env-injected: the token pepper,
- # TOTP key, mandatory PII key, and the Postgres/Redis passwords are projected
- # from `d-oauth-secrets` into a tmpfs volume at `/run/secrets` (`defaultMode:
- # 0400`, group 65532 via `fsGroup`) and read through their `*_FILE`
- # variables. The passwordless DSNs come from the ConfigMap. The inline
- # `D_OAUTH_TOKEN_PEPPER` / `D_OAUTH_TOTP_ENCRYPTION_KEY` forms remain
- # supported and take precedence when both are set (`src/config/build.rs`),
- # but this deployment never sets them, so no secret enters the process
- # environment (`/proc/self/environ`, children, crash dumps, `docker
- # inspect`) — a `kubectl exec` reads nothing from the environment.
- # - The image reference is the mutable `:latest` tag. Nodes pull it on first
- # use (`imagePullPolicy: IfNotPresent`); pin a digest instead when
- # reproducible production rollouts are required.
- apiVersion: apps/v1
- kind: Deployment
- metadata:
- name: d-oauth-server
- namespace: d-oauth
- labels:
- app.kubernetes.io/name: d-oauth-server
- app.kubernetes.io/part-of: d-oauth
- spec:
- replicas: 3
- strategy:
- type: RollingUpdate
- rollingUpdate:
- # Keep every old replica serving while the migration Job + new
- # replicas converge (expand/contract upgrades).
- maxUnavailable: 0
- maxSurge: 1
- selector:
- matchLabels:
- app.kubernetes.io/name: d-oauth-server
- template:
- metadata:
- labels:
- app.kubernetes.io/name: d-oauth-server
- app.kubernetes.io/part-of: d-oauth
- spec:
- terminationGracePeriodSeconds: 45
- # The app never talks to the Kubernetes API; the auto-mounted service
- # account token is pure attack surface, so it stays disabled.
- automountServiceAccountToken: false
- securityContext:
- runAsNonRoot: true
- runAsUser: 65532
- runAsGroup: 65532
- # Secret volumes are tmpfs; fsGroup makes the 0400 files readable by
- # the runtime group so the pepper/TOTP keys can be mounted as files.
- fsGroup: 65532
- fsGroupChangePolicy: OnRootMismatch
- seccompProfile:
- type: RuntimeDefault
- containers:
- - name: server
- # Mutable `:latest` reference: the newest published image is what
- # pods run once the node pulls it.
- image: ghcr.io/syavne/d-oauth-server:latest
- imagePullPolicy: IfNotPresent
- ports:
- - name: public-tls
- containerPort: 8080
- protocol: TCP
- - name: health
- containerPort: 8081
- protocol: TCP
- envFrom:
- - configMapRef:
- name: d-oauth-config
- env:
- # Every secret is a file, never an env value: code execution in
- # the container cannot read the DB/Redis credentials or key
- # material from the environment. The DSNs (non-secret) come from
- # the ConfigMap above.
- - name: D_OAUTH_DATABASE__PASSWORD_FILE
- value: /run/secrets/database-password
- - name: D_OAUTH_REDIS__PASSWORD_FILE
- value: /run/secrets/redis-password
- - name: D_OAUTH_TOKEN_PEPPER_FILE
- value: /run/secrets/token-pepper
- - name: D_OAUTH_TOTP_ENCRYPTION_KEY_FILE
- value: /run/secrets/totp-encryption-key
- - name: D_OAUTH_PII_ENCRYPTION_KEY_FILE
- value: /run/secrets/pii-encryption-key
- # Liveness is dependency-free by construction (`/health/live`
- # touches no Postgres/Redis), so a dependency outage never
- # triggers a restart storm — readiness absorbs it instead.
- startupProbe:
- httpGet:
- path: /health/live
- port: 8081
- failureThreshold: 30
- periodSeconds: 2
- livenessProbe:
- httpGet:
- path: /health/live
- port: 8081
- periodSeconds: 10
- timeoutSeconds: 2
- failureThreshold: 3
- # Fail-closed: 503 while Postgres/Redis are unreachable OR the
- # `_sqlx_migrations` version does not match the binary (i.e. the
- # migrate Job has not run yet on a fresh/rolling deploy).
- readinessProbe:
- httpGet:
- path: /health/ready
- port: 8081
- periodSeconds: 5
- timeoutSeconds: 2
- failureThreshold: 3
- successThreshold: 1
- resources:
- requests:
- cpu: 100m
- memory: 128Mi
- limits:
- cpu: "1"
- memory: 512Mi
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: true
- capabilities:
- drop:
- - ALL
- volumeMounts:
- - name: certs
- mountPath: /certs
- readOnly: true
- - name: secrets
- mountPath: /run/secrets
- readOnly: true
- volumes:
- - name: certs
- secret:
- secretName: d-oauth-tls
- defaultMode: 0o400
- # tmpfs-backed secret volume. `defaultMode: 0400` plus `fsGroup:
- # 65532` leaves the projected files readable only by the runtime
- # user/group. Every secret is projected here; nothing is injected
- # through a `secretKeyRef` env.
- - name: secrets
- secret:
- secretName: d-oauth-secrets
- defaultMode: 0o400
- items:
- - key: database-password
- path: database-password
- - key: redis-password
- path: redis-password
- - key: token-pepper
- path: token-pepper
- - key: totp-encryption-key
- path: totp-encryption-key
- - key: pii-encryption-key
- path: pii-encryption-key
- topologySpreadConstraints:
- - maxSkew: 1
- topologyKey: topology.kubernetes.io/zone
- whenUnsatisfiable: ScheduleAnyway
- labelSelector:
- matchLabels:
- app.kubernetes.io/name: d-oauth-server
- - maxSkew: 1
- topologyKey: kubernetes.io/hostname
- whenUnsatisfiable: ScheduleAnyway
- labelSelector:
- matchLabels:
- app.kubernetes.io/name: d-oauth-server
|