feat: protect Temporal UI with unified admin secret
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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." },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user