feat: protect Temporal UI with unified admin secret

This commit is contained in:
Codex
2026-07-16 18:52:35 +02:00
parent 1d73b98f2c
commit 98bc6192c1
3 changed files with 83 additions and 5 deletions
+7 -1
View File
@@ -13,6 +13,7 @@ defaults:
images:
server: temporalio/auto-setup:1.28.1
ui: temporalio/ui:2.52.1
authProxy: caddy:2.10.2-alpine
pullPolicy: IfNotPresent
targets:
@@ -48,6 +49,11 @@ runtime:
serviceName: temporal-ui
replicas: 1
port: 8080
auth:
username: admin
proxyPort: 8081
secretName: temporal-web-admin
passwordKey: password
logicalNamespaces:
- name: unidesk
retention: 7d
@@ -58,5 +64,5 @@ publicExposure:
publicBaseUrl: https://temporal.hwpod.com
hostname: temporal.hwpod.com
upstream: http://temporal-ui.temporal.svc.cluster.local:8080
healthPath: /
healthPath: /healthz
expectedA: 152.53.229.148
+13
View File
@@ -287,8 +287,21 @@ targets:
- pikaoa-api
timeoutSeconds: 180
pollIntervalSeconds: 5
- id: temporal-nc01
route: NC01:k3s
namespace: temporal
scope: temporal
enabled: true
kubernetesSecrets:
- name: temporal-web-admin
targetId: temporal-nc01
secretName: temporal-web-admin
type: Opaque
data:
- sourceRef: /root/.unidesk/.env/unified-admin-password.txt
sourceKey: contents
targetKey: password
- name: pikaoa-attachments-backup
targetId: pikaoa-nc01
secretName: pikaoa-attachments-backup
+63 -4
View File
@@ -32,7 +32,7 @@ interface TemporalTarget {
interface TemporalConfig {
defaults: { targetId: string };
images: { server: string; ui: string; pullPolicy: string };
images: { server: string; ui: string; authProxy: string; pullPolicy: string };
targets: TemporalTarget[];
database: {
configRef: string;
@@ -47,7 +47,13 @@ interface TemporalConfig {
};
runtime: {
server: { deploymentName: string; serviceName: string; replicas: number; frontendPort: number };
ui: { deploymentName: string; serviceName: string; replicas: number; port: number };
ui: {
deploymentName: string;
serviceName: string;
replicas: number;
port: number;
auth: { username: string; proxyPort: number; secretName: string; passwordKey: string };
};
logicalNamespaces: Array<{ name: string; retention: string }>;
rolloutTimeoutSeconds: number;
};
@@ -114,6 +120,7 @@ function plan(options: ReturnType<typeof parseOpsCommonOptions>): Record<string,
dryRun: `bun scripts/cli.ts platform-infra temporal apply --target ${target.id} --dry-run`,
apply: `bun scripts/cli.ts platform-infra temporal apply --target ${target.id} --confirm`,
publicEdge: `bun scripts/cli.ts platform-infra public-edge plan --target ${target.id}`,
webAdminSecret: "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope temporal --target temporal-nc01 --confirm",
},
};
}
@@ -310,7 +317,7 @@ spec:
ports:
- name: http
port: ${ui.port}
targetPort: http
targetPort: auth
---
apiVersion: apps/v1
kind: Deployment
@@ -355,6 +362,47 @@ spec:
port: http
initialDelaySeconds: 20
periodSeconds: 10
- name: auth-proxy
image: ${temporal.images.authProxy}
imagePullPolicy: ${temporal.images.pullPolicy}
command:
- sh
- -ec
- |
password="$(cat /run/secrets/admin-password)"
hash="$(caddy hash-password --plaintext "$password")"
cat >/tmp/Caddyfile <<EOF
:${ui.auth.proxyPort} {
respond /healthz 200
basic_auth {
${ui.auth.username} $hash
}
reverse_proxy 127.0.0.1:${ui.port}
}
EOF
exec caddy run --config /tmp/Caddyfile --adapter caddyfile
ports:
- name: auth
containerPort: ${ui.auth.proxyPort}
readinessProbe:
tcpSocket:
port: auth
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
tcpSocket:
port: auth
initialDelaySeconds: 15
periodSeconds: 10
volumeMounts:
- name: web-admin-password
mountPath: /run/secrets/admin-password
subPath: ${ui.auth.passwordKey}
readOnly: true
volumes:
- name: web-admin-password
secret:
secretName: ${ui.auth.secretName}
`;
}
@@ -460,12 +508,14 @@ function readTemporalConfig(): TemporalConfig {
const runtime = recordField(root, "runtime", configLabel);
const server = recordField(runtime, "server", `${configLabel}.runtime`);
const ui = recordField(runtime, "ui", `${configLabel}.runtime`);
const uiAuth = recordField(ui, "auth", `${configLabel}.runtime.ui`);
const exposure = recordField(root, "publicExposure", configLabel);
return {
defaults: { targetId: stringField(defaults, "targetId", `${configLabel}.defaults`) },
images: {
server: stringField(images, "server", `${configLabel}.images`),
ui: stringField(images, "ui", `${configLabel}.images`),
authProxy: stringField(images, "authProxy", `${configLabel}.images`),
pullPolicy: stringField(images, "pullPolicy", `${configLabel}.images`),
},
targets: arrayField(root, "targets", configLabel).map((item, index) => ({
@@ -493,7 +543,15 @@ function readTemporalConfig(): TemporalConfig {
},
runtime: {
server: deploymentConfig(server, `${configLabel}.runtime.server`, "frontendPort"),
ui: deploymentConfig(ui, `${configLabel}.runtime.ui`, "port"),
ui: {
...deploymentConfig(ui, `${configLabel}.runtime.ui`, "port"),
auth: {
username: stringField(uiAuth, "username", `${configLabel}.runtime.ui.auth`),
proxyPort: integerField(uiAuth, "proxyPort", `${configLabel}.runtime.ui.auth`),
secretName: stringField(uiAuth, "secretName", `${configLabel}.runtime.ui.auth`),
passwordKey: stringField(uiAuth, "passwordKey", `${configLabel}.runtime.ui.auth`),
},
},
logicalNamespaces: arrayField(runtime, "logicalNamespaces", `${configLabel}.runtime`).map((item, index) => ({
name: stringField(item, "name", `${configLabel}.runtime.logicalNamespaces[${index}]`),
retention: stringField(item, "retention", `${configLabel}.runtime.logicalNamespaces[${index}]`),
@@ -535,6 +593,7 @@ function policyChecks(temporal: TemporalConfig, target: TemporalTarget, manifest
{ name: "no-in-cluster-postgres", ok: !/kind: StatefulSet|PersistentVolumeClaim|image: .*postgres/iu.test(manifest), detail: "Temporal manifest contains no PostgreSQL workload or PVC." },
{ name: "cluster-internal-grpc", ok: !/type: (?:NodePort|LoadBalancer)/u.test(manifest), detail: `${temporal.runtime.server.serviceName}.${target.namespace}.svc.cluster.local` },
{ name: "public-ui-only", ok: temporal.publicExposure.publicBaseUrl === "https://temporal.hwpod.com", detail: temporal.publicExposure.publicBaseUrl },
{ name: "unified-web-admin", ok: temporal.runtime.ui.auth.secretName === "temporal-web-admin", detail: "Web password is distributed from unified-admin-password.txt through the secrets CLI." },
];
}