deployment.yaml 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. # Stateless multi-replica Deployment (task 46).
  2. #
  3. # - 3 replicas sharing one Postgres + one Redis: no replica holds state
  4. # another needs (tokens live in Redis under `{state}`-tagged keys).
  5. # - `serve` NEVER migrates; the separate `migrate-job.yaml` owns the schema.
  6. # Readiness (`/health/ready`) compares `_sqlx_migrations` against the
  7. # binary's embedded migrations, so new replicas stay out of traffic until
  8. # the Job has applied the schema (expand/contract keeps N-1 serving).
  9. # - Probes hit the PLAINTEXT pod-IP health listener (`health_addr`,
  10. # default `0.0.0.0:8081`); the public listener stays TLS-only.
  11. # - `terminationGracePeriodSeconds: 45` covers the server's 1s propagation
  12. # grace + 30s drain budget (`src/serve.rs`: PROPAGATION_GRACE, DRAIN_DEADLINE).
  13. # - Host hardening: non-root 65532, read-only rootfs, ALL capabilities
  14. # dropped, `allowPrivilegeEscalation: false`, `RuntimeDefault` seccomp, and
  15. # `automountServiceAccountToken: false` (the server never calls the
  16. # Kubernetes API). `networkpolicy.yaml` default-denies pod egress except DNS
  17. # + Postgres + Redis.
  18. # - Secret material is file-mounted, never env-injected: the token pepper,
  19. # TOTP key, mandatory PII key, and the Postgres/Redis passwords are projected
  20. # from `d-oauth-secrets` into a tmpfs volume at `/run/secrets` (`defaultMode:
  21. # 0400`, group 65532 via `fsGroup`) and read through their `*_FILE`
  22. # variables. The passwordless DSNs come from the ConfigMap. The inline
  23. # `D_OAUTH_TOKEN_PEPPER` / `D_OAUTH_TOTP_ENCRYPTION_KEY` forms remain
  24. # supported and take precedence when both are set (`src/config/build.rs`),
  25. # but this deployment never sets them, so no secret enters the process
  26. # environment (`/proc/self/environ`, children, crash dumps, `docker
  27. # inspect`) — a `kubectl exec` reads nothing from the environment.
  28. # - The image reference is the mutable `:latest` tag. Nodes pull it on first
  29. # use (`imagePullPolicy: IfNotPresent`); pin a digest instead when
  30. # reproducible production rollouts are required.
  31. apiVersion: apps/v1
  32. kind: Deployment
  33. metadata:
  34. name: d-oauth-server
  35. namespace: d-oauth
  36. labels:
  37. app.kubernetes.io/name: d-oauth-server
  38. app.kubernetes.io/part-of: d-oauth
  39. spec:
  40. replicas: 3
  41. strategy:
  42. type: RollingUpdate
  43. rollingUpdate:
  44. # Keep every old replica serving while the migration Job + new
  45. # replicas converge (expand/contract upgrades).
  46. maxUnavailable: 0
  47. maxSurge: 1
  48. selector:
  49. matchLabels:
  50. app.kubernetes.io/name: d-oauth-server
  51. template:
  52. metadata:
  53. labels:
  54. app.kubernetes.io/name: d-oauth-server
  55. app.kubernetes.io/part-of: d-oauth
  56. spec:
  57. terminationGracePeriodSeconds: 45
  58. # The app never talks to the Kubernetes API; the auto-mounted service
  59. # account token is pure attack surface, so it stays disabled.
  60. automountServiceAccountToken: false
  61. securityContext:
  62. runAsNonRoot: true
  63. runAsUser: 65532
  64. runAsGroup: 65532
  65. # Secret volumes are tmpfs; fsGroup makes the 0400 files readable by
  66. # the runtime group so the pepper/TOTP keys can be mounted as files.
  67. fsGroup: 65532
  68. fsGroupChangePolicy: OnRootMismatch
  69. seccompProfile:
  70. type: RuntimeDefault
  71. containers:
  72. - name: server
  73. # Mutable `:latest` reference: the newest published image is what
  74. # pods run once the node pulls it.
  75. image: ghcr.io/syavne/d-oauth-server:latest
  76. imagePullPolicy: IfNotPresent
  77. ports:
  78. - name: public-tls
  79. containerPort: 8080
  80. protocol: TCP
  81. - name: health
  82. containerPort: 8081
  83. protocol: TCP
  84. envFrom:
  85. - configMapRef:
  86. name: d-oauth-config
  87. env:
  88. # Every secret is a file, never an env value: code execution in
  89. # the container cannot read the DB/Redis credentials or key
  90. # material from the environment. The DSNs (non-secret) come from
  91. # the ConfigMap above.
  92. - name: D_OAUTH_DATABASE__PASSWORD_FILE
  93. value: /run/secrets/database-password
  94. - name: D_OAUTH_REDIS__PASSWORD_FILE
  95. value: /run/secrets/redis-password
  96. - name: D_OAUTH_TOKEN_PEPPER_FILE
  97. value: /run/secrets/token-pepper
  98. - name: D_OAUTH_TOTP_ENCRYPTION_KEY_FILE
  99. value: /run/secrets/totp-encryption-key
  100. - name: D_OAUTH_PII_ENCRYPTION_KEY_FILE
  101. value: /run/secrets/pii-encryption-key
  102. # Liveness is dependency-free by construction (`/health/live`
  103. # touches no Postgres/Redis), so a dependency outage never
  104. # triggers a restart storm — readiness absorbs it instead.
  105. startupProbe:
  106. httpGet:
  107. path: /health/live
  108. port: 8081
  109. failureThreshold: 30
  110. periodSeconds: 2
  111. livenessProbe:
  112. httpGet:
  113. path: /health/live
  114. port: 8081
  115. periodSeconds: 10
  116. timeoutSeconds: 2
  117. failureThreshold: 3
  118. # Fail-closed: 503 while Postgres/Redis are unreachable OR the
  119. # `_sqlx_migrations` version does not match the binary (i.e. the
  120. # migrate Job has not run yet on a fresh/rolling deploy).
  121. readinessProbe:
  122. httpGet:
  123. path: /health/ready
  124. port: 8081
  125. periodSeconds: 5
  126. timeoutSeconds: 2
  127. failureThreshold: 3
  128. successThreshold: 1
  129. resources:
  130. requests:
  131. cpu: 100m
  132. memory: 128Mi
  133. limits:
  134. cpu: "1"
  135. memory: 512Mi
  136. securityContext:
  137. allowPrivilegeEscalation: false
  138. readOnlyRootFilesystem: true
  139. capabilities:
  140. drop:
  141. - ALL
  142. volumeMounts:
  143. - name: certs
  144. mountPath: /certs
  145. readOnly: true
  146. - name: secrets
  147. mountPath: /run/secrets
  148. readOnly: true
  149. volumes:
  150. - name: certs
  151. secret:
  152. secretName: d-oauth-tls
  153. defaultMode: 0o400
  154. # tmpfs-backed secret volume. `defaultMode: 0400` plus `fsGroup:
  155. # 65532` leaves the projected files readable only by the runtime
  156. # user/group. Every secret is projected here; nothing is injected
  157. # through a `secretKeyRef` env.
  158. - name: secrets
  159. secret:
  160. secretName: d-oauth-secrets
  161. defaultMode: 0o400
  162. items:
  163. - key: database-password
  164. path: database-password
  165. - key: redis-password
  166. path: redis-password
  167. - key: token-pepper
  168. path: token-pepper
  169. - key: totp-encryption-key
  170. path: totp-encryption-key
  171. - key: pii-encryption-key
  172. path: pii-encryption-key
  173. topologySpreadConstraints:
  174. - maxSkew: 1
  175. topologyKey: topology.kubernetes.io/zone
  176. whenUnsatisfiable: ScheduleAnyway
  177. labelSelector:
  178. matchLabels:
  179. app.kubernetes.io/name: d-oauth-server
  180. - maxSkew: 1
  181. topologyKey: kubernetes.io/hostname
  182. whenUnsatisfiable: ScheduleAnyway
  183. labelSelector:
  184. matchLabels:
  185. app.kubernetes.io/name: d-oauth-server