feat(auth): scaffold Ory Kratos identity service — phase 1 (infrastructure)
First phase of replacing the hand-rolled auth (internal/services/auth_service.go
et al.) with Ory Kratos. This commit is infrastructure only — Kratos will run
but nothing consumes it yet; the Go API still does its own auth until phase 2.
Adds deploy-k3s/manifests/kratos/:
- configmap.yaml — kratos.yml, identity schema, Google/Apple OIDC claim
mappers (no secrets in the ConfigMap)
- migrate-job.yaml — `kratos migrate sql`, run before the Deployment
- kratos.yaml — Deployment (x2), Service, NetworkPolicies
- ingress.yaml — auth.myhoneydue.com -> Kratos public API :4433
- README.md — operator prerequisites + deploy runbook
Wiring:
- 02-setup-secrets.sh creates kratos-secrets, gated on a config.yaml `kratos:`
block (DSN, cookie/cipher, SMTP URI, OIDC client secret, Apple key).
- 03-deploy.sh applies the Kratos manifests + runs the migrate Job, gated on
the kratos-secrets Secret existing.
Both gates mean the existing stack deploys completely unaffected until the
operator completes the prerequisites (Neon `kratos` DB, auth.myhoneydue.com
DNS, Apple/Google OAuth apps, Kratos image version). Pre-production, so no
user-data migration — see manifests/kratos/README.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
# Ory Kratos — honeyDue identity service (Phase 1: infrastructure)
|
||||||
|
|
||||||
|
This directory deploys [Ory Kratos](https://www.ory.sh/kratos/) into the
|
||||||
|
`honeydue` namespace as the identity provider — replacing the hand-rolled auth
|
||||||
|
in `internal/services/auth_service.go` etc.
|
||||||
|
|
||||||
|
**Phase 1 is infrastructure only.** Once deployed, Kratos runs but nothing uses
|
||||||
|
it yet — the honeyDue Go API still does its own auth. Phase 2 (backend swap)
|
||||||
|
and Phase 3 (KMP/web clients) follow. Migrating onto Kratos can lose all
|
||||||
|
existing user data — honeyDue is pre-production, so no user import is done.
|
||||||
|
|
||||||
|
The deploy is **gated**: `03-deploy.sh` applies Kratos only when the
|
||||||
|
`kratos-secrets` Secret exists, and `02-setup-secrets.sh` creates that Secret
|
||||||
|
only when `config.yaml` has a `kratos:` block. Until then the existing stack
|
||||||
|
deploys completely unaffected.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | What |
|
||||||
|
|---|---|
|
||||||
|
| `configmap.yaml` | `kratos.yml`, identity schema, Google/Apple OIDC claim mappers (no secrets) |
|
||||||
|
| `migrate-job.yaml` | `kratos migrate sql` — schema migration, run before the Deployment |
|
||||||
|
| `kratos.yaml` | Deployment (×2), Service, NetworkPolicies |
|
||||||
|
| `ingress.yaml` | `auth.myhoneydue.com` → Kratos public API :4433 |
|
||||||
|
|
||||||
|
## Operator prerequisites (must be done before deploying)
|
||||||
|
|
||||||
|
1. **Kratos version** — Ory uses CalVer (`v25.x` / `v26.x`). Pick the current
|
||||||
|
stable, then replace `REPLACE_WITH_CURRENT_STABLE_TAG` in `kratos.yaml` and
|
||||||
|
`migrate-job.yaml` with `oryd/kratos:vXX.Y@sha256:<digest>`, and set the
|
||||||
|
matching `version:` in `configmap.yaml`.
|
||||||
|
|
||||||
|
2. **Kratos database** — create a separate Neon database named `kratos` (do not
|
||||||
|
share honeyDue's). Capture its connection string as the DSN.
|
||||||
|
|
||||||
|
3. **DNS** — add `auth.myhoneydue.com` in Cloudflare (proxied), pointing at the
|
||||||
|
cluster ingress like the other honeyDue hosts. Confirm the
|
||||||
|
`cloudflare-origin-cert` TLS secret covers `auth.myhoneydue.com`.
|
||||||
|
|
||||||
|
4. **Google OAuth client** — Google Cloud Console → create an OAuth 2.0 client.
|
||||||
|
Redirect URI: `https://auth.myhoneydue.com/self-service/methods/oidc/callback/google`.
|
||||||
|
Put the **client ID** into `configmap.yaml` (`GOOGLE_OAUTH_CLIENT_ID`); the
|
||||||
|
**client secret** goes in `config.yaml`.
|
||||||
|
|
||||||
|
5. **Apple Sign In** — Apple Developer → a Services ID + a Sign in with Apple
|
||||||
|
key. Return URL: `https://auth.myhoneydue.com/self-service/methods/oidc/callback/apple`.
|
||||||
|
Put the **Services ID / Team ID / Key ID** into `configmap.yaml`
|
||||||
|
(`APPLE_SERVICES_ID` / `APPLE_TEAM_ID` / `APPLE_PRIVATE_KEY_ID`); the **.p8
|
||||||
|
private key** goes in `config.yaml`.
|
||||||
|
|
||||||
|
6. **`config.yaml`** — add a `kratos:` block:
|
||||||
|
```yaml
|
||||||
|
kratos:
|
||||||
|
dsn: "postgres://USER:PASS@HOST/kratos?sslmode=require"
|
||||||
|
secrets_cookie: "<openssl rand -hex 16>" # generate ONCE, keep stable
|
||||||
|
secrets_cipher: "<openssl rand -hex 16>" # must be exactly 32 chars
|
||||||
|
smtp_connection_uri: "smtps://USER:PASS@smtp.fastmail.com:465/"
|
||||||
|
google_client_secret: "<from Google Cloud Console>"
|
||||||
|
apple_private_key: |
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
...
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
```
|
||||||
|
`secrets_cookie` / `secrets_cipher` must stay stable forever — rotating them
|
||||||
|
invalidates every session and makes encrypted data unreadable.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd honeyDueAPI-go
|
||||||
|
export KUBECONFIG="$(pwd)/deploy-k3s/kubeconfig"
|
||||||
|
./deploy-k3s/scripts/02-setup-secrets.sh # creates kratos-secrets from config.yaml
|
||||||
|
./deploy-k3s/scripts/03-deploy.sh # applies kratos manifests, runs migrate, rolls
|
||||||
|
```
|
||||||
|
|
||||||
|
`03-deploy.sh` applies `configmap.yaml` → runs `migrate-job.yaml` → waits →
|
||||||
|
applies `kratos.yaml` + `ingress.yaml`.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
- `kubectl -n honeydue get pods -l app.kubernetes.io/name=kratos` — 2/2 Running
|
||||||
|
- `kubectl -n honeydue logs job/kratos-migrate` — migration succeeded
|
||||||
|
- `curl https://auth.myhoneydue.com/health/ready` — `{"status":"ok"}`
|
||||||
|
- `curl https://auth.myhoneydue.com/self-service/registration/api` — returns a flow
|
||||||
|
|
||||||
|
## Not yet done (later phases)
|
||||||
|
|
||||||
|
- **Phase 2** — honeyDue Go backend: swap `middleware/auth.go` for Kratos
|
||||||
|
session validation, drop the hand-rolled auth code, rebuild the `users`
|
||||||
|
table keyed on the Kratos identity ID.
|
||||||
|
- **Phase 3** — KMP mobile + Next.js web clients point at Kratos flows.
|
||||||
|
- Admin-panel auth stays on its own JWT (out of scope).
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# Ory Kratos configuration for honeyDue.
|
||||||
|
#
|
||||||
|
# Secrets are NOT in this ConfigMap. The DSN, cookie/cipher secrets, SMTP URI
|
||||||
|
# and OIDC client secrets are injected as environment variables from the
|
||||||
|
# kratos-secrets Secret (see kratos.yaml). Kratos is configured natively via
|
||||||
|
# env vars, so this is the idiomatic split — only non-secret config here.
|
||||||
|
#
|
||||||
|
# OPERATOR: replace the GOOGLE_OAUTH_CLIENT_ID / APPLE_* client-id placeholders
|
||||||
|
# below with the real (non-secret) OAuth client identifiers once the Apple and
|
||||||
|
# Google OAuth apps exist. The matching secrets go in kratos-secrets.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: kratos-config
|
||||||
|
namespace: honeydue
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
app.kubernetes.io/part-of: honeydue
|
||||||
|
data:
|
||||||
|
kratos.yml: |
|
||||||
|
# version must track the Kratos image tag — confirm against the deployed
|
||||||
|
# Kratos release (Ory uses CalVer, e.g. v26.x). See kratos/README.md.
|
||||||
|
version: v1.3.0
|
||||||
|
|
||||||
|
serve:
|
||||||
|
public:
|
||||||
|
base_url: https://auth.myhoneydue.com/
|
||||||
|
cors:
|
||||||
|
enabled: true
|
||||||
|
allowed_origins:
|
||||||
|
- https://myhoneydue.com
|
||||||
|
- https://app.myhoneydue.com
|
||||||
|
- https://admin.myhoneydue.com
|
||||||
|
allowed_methods: [GET, POST, PUT, PATCH, DELETE]
|
||||||
|
allowed_headers: [Authorization, Content-Type, X-Session-Token, Cookie]
|
||||||
|
exposed_headers: [Content-Type, Set-Cookie]
|
||||||
|
admin:
|
||||||
|
base_url: http://kratos.honeydue.svc.cluster.local:4434/
|
||||||
|
|
||||||
|
selfservice:
|
||||||
|
default_browser_return_url: https://app.myhoneydue.com/
|
||||||
|
allowed_return_urls:
|
||||||
|
- https://app.myhoneydue.com
|
||||||
|
- https://myhoneydue.com
|
||||||
|
- honeydue://callback
|
||||||
|
|
||||||
|
methods:
|
||||||
|
password:
|
||||||
|
enabled: true
|
||||||
|
code: # email one-time codes (verify/recover)
|
||||||
|
enabled: true
|
||||||
|
oidc:
|
||||||
|
enabled: true
|
||||||
|
config:
|
||||||
|
providers:
|
||||||
|
# index 0 — Google. client_secret is injected via env var
|
||||||
|
# SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_0_CLIENT_SECRET.
|
||||||
|
- id: google
|
||||||
|
provider: google
|
||||||
|
client_id: GOOGLE_OAUTH_CLIENT_ID
|
||||||
|
mapper_url: file:///etc/kratos/oidc.google.jsonnet
|
||||||
|
scope: [openid, email, profile]
|
||||||
|
# index 1 — Apple. apple_private_key is injected via env var
|
||||||
|
# SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_1_APPLE_PRIVATE_KEY.
|
||||||
|
- id: apple
|
||||||
|
provider: apple
|
||||||
|
client_id: APPLE_SERVICES_ID
|
||||||
|
apple_team_id: APPLE_TEAM_ID
|
||||||
|
apple_private_key_id: APPLE_PRIVATE_KEY_ID
|
||||||
|
mapper_url: file:///etc/kratos/oidc.apple.jsonnet
|
||||||
|
scope: [openid, email, name]
|
||||||
|
|
||||||
|
flows:
|
||||||
|
error:
|
||||||
|
ui_url: https://app.myhoneydue.com/auth/error
|
||||||
|
login:
|
||||||
|
ui_url: https://app.myhoneydue.com/auth/login
|
||||||
|
lifespan: 10m
|
||||||
|
registration:
|
||||||
|
ui_url: https://app.myhoneydue.com/auth/registration
|
||||||
|
lifespan: 10m
|
||||||
|
after:
|
||||||
|
password:
|
||||||
|
hooks:
|
||||||
|
- hook: session # auto-login after registration
|
||||||
|
oidc:
|
||||||
|
hooks:
|
||||||
|
- hook: session
|
||||||
|
verification:
|
||||||
|
enabled: true
|
||||||
|
ui_url: https://app.myhoneydue.com/auth/verification
|
||||||
|
use: code
|
||||||
|
after:
|
||||||
|
default_browser_return_url: https://app.myhoneydue.com/
|
||||||
|
recovery:
|
||||||
|
enabled: true
|
||||||
|
ui_url: https://app.myhoneydue.com/auth/recovery
|
||||||
|
use: code
|
||||||
|
settings:
|
||||||
|
ui_url: https://app.myhoneydue.com/auth/settings
|
||||||
|
privileged_session_max_age: 15m
|
||||||
|
logout:
|
||||||
|
after:
|
||||||
|
default_browser_return_url: https://app.myhoneydue.com/
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
format: json
|
||||||
|
leak_sensitive_values: false
|
||||||
|
|
||||||
|
ciphers:
|
||||||
|
algorithm: xchacha20-poly1305
|
||||||
|
|
||||||
|
hashers:
|
||||||
|
algorithm: bcrypt
|
||||||
|
bcrypt:
|
||||||
|
cost: 12
|
||||||
|
|
||||||
|
identity:
|
||||||
|
default_schema_id: honeydue
|
||||||
|
schemas:
|
||||||
|
- id: honeydue
|
||||||
|
url: file:///etc/kratos/identity.schema.json
|
||||||
|
|
||||||
|
courier:
|
||||||
|
smtp:
|
||||||
|
from_address: noreply@myhoneydue.com
|
||||||
|
from_name: honeyDue
|
||||||
|
# connection_uri is injected via env COURIER_SMTP_CONNECTION_URI
|
||||||
|
|
||||||
|
session:
|
||||||
|
lifespan: 720h # 30-day sessions (mobile)
|
||||||
|
cookie:
|
||||||
|
domain: myhoneydue.com
|
||||||
|
same_site: Lax
|
||||||
|
|
||||||
|
identity.schema.json: |
|
||||||
|
{
|
||||||
|
"$id": "https://honeydue.app/identity.schema.json",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "honeyDue user",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"traits": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "email",
|
||||||
|
"title": "Email",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 320,
|
||||||
|
"ory.sh/kratos": {
|
||||||
|
"credentials": {
|
||||||
|
"password": { "identifier": true },
|
||||||
|
"code": { "identifier": true, "via": "email" },
|
||||||
|
"totp": { "account_name": true }
|
||||||
|
},
|
||||||
|
"verification": { "via": "email" },
|
||||||
|
"recovery": { "via": "email" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "Name",
|
||||||
|
"properties": {
|
||||||
|
"first": { "type": "string", "title": "First name", "maxLength": 100 },
|
||||||
|
"last": { "type": "string", "title": "Last name", "maxLength": 100 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["email"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oidc.google.jsonnet: |
|
||||||
|
// Maps Google OIDC claims onto the honeyDue identity schema.
|
||||||
|
local claims = std.extVar('claims');
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
traits: {
|
||||||
|
email: claims.email,
|
||||||
|
[if 'given_name' in claims || 'family_name' in claims then 'name']: {
|
||||||
|
first: if 'given_name' in claims then claims.given_name else '',
|
||||||
|
last: if 'family_name' in claims then claims.family_name else '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
oidc.apple.jsonnet: |
|
||||||
|
// Maps Apple OIDC claims onto the honeyDue identity schema. Apple only
|
||||||
|
// returns the name on the very first authorization and not in the ID
|
||||||
|
// token claims, so only email is mapped here.
|
||||||
|
local claims = std.extVar('claims');
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
traits: {
|
||||||
|
email: claims.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Public ingress for Ory Kratos — auth.myhoneydue.com → Kratos public API :4433.
|
||||||
|
#
|
||||||
|
# Chains the same edge middlewares as the honeyDue API ingress: cloudflare-only
|
||||||
|
# (reject non-Cloudflare source IPs), security-headers, and the general
|
||||||
|
# rate-limit. Kratos's self-service flows are multi-request, so the strict
|
||||||
|
# auth-rate-limit (5/min) is intentionally NOT used here — Kratos applies its
|
||||||
|
# own per-flow protections.
|
||||||
|
#
|
||||||
|
# OPERATOR: confirm the cloudflare-origin-cert TLS secret covers
|
||||||
|
# auth.myhoneydue.com (apex + wildcard origin cert), and add the
|
||||||
|
# auth.myhoneydue.com DNS record in Cloudflare (proxied) → cluster ingress.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: honeydue-auth
|
||||||
|
namespace: honeydue
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
app.kubernetes.io/part-of: honeydue
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: honeydue-cloudflare-only@kubernetescrd,honeydue-security-headers@kubernetescrd,honeydue-rate-limit@kubernetescrd
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- auth.myhoneydue.com
|
||||||
|
secretName: cloudflare-origin-cert
|
||||||
|
rules:
|
||||||
|
- host: auth.myhoneydue.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: kratos
|
||||||
|
port:
|
||||||
|
number: 4433
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# Ory Kratos — identity service for honeyDue.
|
||||||
|
#
|
||||||
|
# Deployed only once the operator has completed the prerequisites in
|
||||||
|
# kratos/README.md (Neon `kratos` database, auth.myhoneydue.com DNS, Apple +
|
||||||
|
# Google OAuth apps, and the kratos-secrets Secret). Until then 03-deploy.sh
|
||||||
|
# skips the Kratos apply, so the existing stack is unaffected.
|
||||||
|
#
|
||||||
|
# IMAGE: oryd/kratos uses CalVer (v25.x / v26.x). The tag below is a
|
||||||
|
# fail-loud placeholder — set the current stable tag and pin a @sha256:
|
||||||
|
# digest (like redis/vmagent) before deploying. See kratos/README.md.
|
||||||
|
# The schema-migration Job is in migrate-job.yaml (run before this).
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: kratos
|
||||||
|
namespace: honeydue
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
app.kubernetes.io/part-of: honeydue
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxUnavailable: 0
|
||||||
|
maxSurge: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
app.kubernetes.io/part-of: honeydue
|
||||||
|
spec:
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: kratos
|
||||||
|
image: oryd/kratos:REPLACE_WITH_CURRENT_STABLE_TAG
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
args:
|
||||||
|
- serve
|
||||||
|
- --config
|
||||||
|
- /etc/kratos/kratos.yml
|
||||||
|
- --watch-courier # send verification/recovery email in-process
|
||||||
|
ports:
|
||||||
|
- name: public
|
||||||
|
containerPort: 4433
|
||||||
|
- name: admin
|
||||||
|
containerPort: 4434
|
||||||
|
env:
|
||||||
|
# Kratos is configured natively via env vars; secrets come from
|
||||||
|
# the kratos-secrets Secret rather than the ConfigMap.
|
||||||
|
- name: DSN
|
||||||
|
valueFrom: { secretKeyRef: { name: kratos-secrets, key: dsn } }
|
||||||
|
- name: SECRETS_COOKIE
|
||||||
|
valueFrom: { secretKeyRef: { name: kratos-secrets, key: secrets_cookie } }
|
||||||
|
- name: SECRETS_CIPHER
|
||||||
|
valueFrom: { secretKeyRef: { name: kratos-secrets, key: secrets_cipher } }
|
||||||
|
- name: COURIER_SMTP_CONNECTION_URI
|
||||||
|
valueFrom: { secretKeyRef: { name: kratos-secrets, key: smtp_connection_uri } }
|
||||||
|
# OIDC provider secrets — index must match the providers list
|
||||||
|
# order in configmap.yaml (0 = google, 1 = apple).
|
||||||
|
- name: SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_0_CLIENT_SECRET
|
||||||
|
valueFrom: { secretKeyRef: { name: kratos-secrets, key: google_client_secret } }
|
||||||
|
- name: SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_1_APPLE_PRIVATE_KEY
|
||||||
|
valueFrom: { secretKeyRef: { name: kratos-secrets, key: apple_private_key } }
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/kratos
|
||||||
|
readOnly: true
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health/ready
|
||||||
|
port: 4434
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health/alive
|
||||||
|
port: 4434
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 512Mi
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: kratos-config
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 64Mi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: kratos
|
||||||
|
namespace: honeydue
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
app.kubernetes.io/part-of: honeydue
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
ports:
|
||||||
|
- name: public
|
||||||
|
port: 4433
|
||||||
|
targetPort: 4433
|
||||||
|
- name: admin
|
||||||
|
port: 4434
|
||||||
|
targetPort: 4434
|
||||||
|
---
|
||||||
|
# Ingress to Kratos: Traefik (the auth.myhoneydue.com IngressRoute) and the
|
||||||
|
# honeyDue api pods (session whoami) may reach the public API :4433. The
|
||||||
|
# admin API :4434 takes no cluster ingress — it is reachable only in-pod.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-ingress-to-kratos
|
||||||
|
namespace: honeydue
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: api
|
||||||
|
ports:
|
||||||
|
- port: 4433
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
# Kratos egress: DNS, the Neon Postgres database, SMTP, and HTTPS to the
|
||||||
|
# OIDC providers (Apple/Google token + JWKS endpoints).
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-egress-from-kratos
|
||||||
|
namespace: honeydue
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
policyTypes:
|
||||||
|
- Egress
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector: {}
|
||||||
|
ports:
|
||||||
|
- port: 53
|
||||||
|
protocol: UDP
|
||||||
|
- port: 53
|
||||||
|
protocol: TCP
|
||||||
|
# Neon Postgres (external)
|
||||||
|
- to:
|
||||||
|
- ipBlock:
|
||||||
|
cidr: 0.0.0.0/0
|
||||||
|
except:
|
||||||
|
- 10.42.0.0/16
|
||||||
|
- 10.43.0.0/16
|
||||||
|
ports:
|
||||||
|
- port: 5432
|
||||||
|
protocol: TCP
|
||||||
|
# SMTP (Fastmail) + HTTPS to Apple/Google OIDC endpoints (external)
|
||||||
|
- to:
|
||||||
|
- ipBlock:
|
||||||
|
cidr: 0.0.0.0/0
|
||||||
|
except:
|
||||||
|
- 10.42.0.0/16
|
||||||
|
- 10.43.0.0/16
|
||||||
|
ports:
|
||||||
|
- port: 465
|
||||||
|
protocol: TCP
|
||||||
|
- port: 443
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Ory Kratos schema migration — runs `kratos migrate sql` against the Kratos
|
||||||
|
# database before the Kratos Deployment rolls. 03-deploy.sh applies this,
|
||||||
|
# waits for completion, then applies kratos.yaml.
|
||||||
|
#
|
||||||
|
# IMAGE: set the same oryd/kratos tag as kratos.yaml (Ory CalVer v25.x/v26.x);
|
||||||
|
# pin a @sha256: digest. See kratos/README.md.
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: kratos-migrate
|
||||||
|
namespace: honeydue
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
app.kubernetes.io/part-of: honeydue
|
||||||
|
spec:
|
||||||
|
backoffLimit: 0
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: kratos
|
||||||
|
app.kubernetes.io/part-of: honeydue
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: kratos-migrate
|
||||||
|
image: oryd/kratos:REPLACE_WITH_CURRENT_STABLE_TAG
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
args: ["migrate", "sql", "-e", "--yes"]
|
||||||
|
env:
|
||||||
|
- name: DSN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: kratos-secrets
|
||||||
|
key: dsn
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
@@ -164,6 +164,35 @@ else
|
|||||||
warn "Admin panel will NOT have basic auth protection."
|
warn "Admin panel will NOT have basic auth protection."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Create Kratos secrets (Ory Kratos identity service) ---
|
||||||
|
# Created only when config.yaml has a kratos.dsn. Until then 03-deploy.sh skips
|
||||||
|
# the Kratos deploy entirely, so the existing stack is unaffected.
|
||||||
|
|
||||||
|
KRATOS_DSN="$(cfg kratos.dsn 2>/dev/null || true)"
|
||||||
|
if [[ -n "${KRATOS_DSN}" ]]; then
|
||||||
|
log "Creating kratos-secrets..."
|
||||||
|
KR_COOKIE="$(cfg kratos.secrets_cookie 2>/dev/null || true)"
|
||||||
|
KR_CIPHER="$(cfg kratos.secrets_cipher 2>/dev/null || true)"
|
||||||
|
KR_SMTP="$(cfg kratos.smtp_connection_uri 2>/dev/null || true)"
|
||||||
|
KR_GOOGLE="$(cfg kratos.google_client_secret 2>/dev/null || true)"
|
||||||
|
KR_APPLE="$(cfg kratos.apple_private_key 2>/dev/null || true)"
|
||||||
|
[[ -n "${KR_COOKIE}" && -n "${KR_CIPHER}" ]] \
|
||||||
|
|| die "kratos.secrets_cookie / secrets_cipher must be set (generate once: openssl rand -hex 16)"
|
||||||
|
[[ ${#KR_CIPHER} -eq 32 ]] \
|
||||||
|
|| die "kratos.secrets_cipher must be exactly 32 characters (openssl rand -hex 16)"
|
||||||
|
kubectl create secret generic kratos-secrets \
|
||||||
|
--namespace="${NAMESPACE}" \
|
||||||
|
--from-literal="dsn=${KRATOS_DSN}" \
|
||||||
|
--from-literal="secrets_cookie=${KR_COOKIE}" \
|
||||||
|
--from-literal="secrets_cipher=${KR_CIPHER}" \
|
||||||
|
--from-literal="smtp_connection_uri=${KR_SMTP}" \
|
||||||
|
--from-literal="google_client_secret=${KR_GOOGLE}" \
|
||||||
|
--from-literal="apple_private_key=${KR_APPLE}" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
else
|
||||||
|
warn "config.yaml has no kratos.dsn — skipping kratos-secrets (Kratos not yet configured)."
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Done ---
|
# --- Done ---
|
||||||
|
|
||||||
log ""
|
log ""
|
||||||
|
|||||||
@@ -264,6 +264,27 @@ if [[ -d "${MANIFESTS}/observability" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Ory Kratos (identity service) ---
|
||||||
|
# Applied only when kratos-secrets exists — i.e. the operator has completed the
|
||||||
|
# Kratos prerequisites in deploy-k3s/manifests/kratos/README.md. Otherwise
|
||||||
|
# skipped, so the existing stack deploys unaffected.
|
||||||
|
if kubectl -n "${NAMESPACE}" get secret kratos-secrets >/dev/null 2>&1; then
|
||||||
|
log "Deploying Ory Kratos..."
|
||||||
|
kubectl apply -f "${MANIFESTS}/kratos/configmap.yaml"
|
||||||
|
# The migrate Job is immutable — delete any prior run, then apply + wait.
|
||||||
|
kubectl delete job kratos-migrate -n "${NAMESPACE}" --ignore-not-found --wait=true >/dev/null
|
||||||
|
kubectl apply -f "${MANIFESTS}/kratos/migrate-job.yaml"
|
||||||
|
if ! kubectl wait --namespace="${NAMESPACE}" --for=condition=complete --timeout=5m job/kratos-migrate; then
|
||||||
|
warn "Kratos migration Job failed — logs:"
|
||||||
|
kubectl logs -n "${NAMESPACE}" job/kratos-migrate --tail=100 || true
|
||||||
|
die "aborting: Kratos schema migration failed"
|
||||||
|
fi
|
||||||
|
kubectl apply -f "${MANIFESTS}/kratos/kratos.yaml"
|
||||||
|
kubectl apply -f "${MANIFESTS}/kratos/ingress.yaml"
|
||||||
|
else
|
||||||
|
log "kratos-secrets not present — skipping Kratos deploy (see manifests/kratos/README.md)."
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Wait for rollouts ---
|
# --- Wait for rollouts ---
|
||||||
|
|
||||||
log "Waiting for rollouts..."
|
log "Waiting for rollouts..."
|
||||||
@@ -281,6 +302,9 @@ fi
|
|||||||
if kubectl -n "${NAMESPACE}" get daemonset alloy-logs >/dev/null 2>&1; then
|
if kubectl -n "${NAMESPACE}" get daemonset alloy-logs >/dev/null 2>&1; then
|
||||||
kubectl rollout status daemonset/alloy-logs -n "${NAMESPACE}" --timeout=120s
|
kubectl rollout status daemonset/alloy-logs -n "${NAMESPACE}" --timeout=120s
|
||||||
fi
|
fi
|
||||||
|
if kubectl -n "${NAMESPACE}" get deployment kratos >/dev/null 2>&1; then
|
||||||
|
kubectl rollout status deployment/kratos -n "${NAMESPACE}" --timeout=180s
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Done ---
|
# --- Done ---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user