From fcb80f1f37e0e0e13ad7e62c7813cfc7ead09baa Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Sat, 4 Apr 2026 12:53:27 +0100 Subject: [PATCH] feat(devtools): deploy Penpot + MCP server, wildcard TLS via DNS-01 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Penpot (designer.sunbeam.pt): - Frontend/backend/exporter deployments with OIDC-only auth via Hydra - VSO-managed DB, S3, and app secrets from OpenBao - PostgreSQL user/db in CNPG postInitSQL - Hydra Maester enabledNamespaces extended to devtools Penpot MCP server (mcp-designer.sunbeam.pt): - Pre-built Node.js image pushed to Gitea registry - Auth-gated via Pingora auth_request → Hydra /userinfo - WebSocket path for browser plugin connection Wildcard TLS: - Switched cert-manager from HTTP-01 (per-SAN) to DNS-01 via Scaleway webhook - Certificate collapsed to *.sunbeam.pt + sunbeam.pt - Added scaleway-certmanager-webhook Helm chart - VSO secret for Scaleway DNS API credentials in cert-manager namespace - Added cert-manager to OpenBao VSO auth role --- base/cert-manager/kustomization.yaml | 9 + base/cert-manager/scaleway-dns-secret.yaml | 37 ++++ base/data/postgres-cluster.yaml | 2 + base/devtools/kustomization.yaml | 4 + base/devtools/penpot-mcp.Dockerfile | 12 ++ base/devtools/penpot-mcp.yaml | 56 ++++++ base/devtools/penpot-oidc.yaml | 20 ++ base/devtools/penpot.yaml | 205 +++++++++++++++++++++ base/devtools/vault-secrets.yaml | 78 ++++++++ base/ingress/pingora-config.yaml | 23 +++ base/ory/hydra-values.yaml | 1 + overlays/production/cert-manager.yaml | 66 +++---- scripts/local-seed-secrets.sh | 13 +- 13 files changed, 486 insertions(+), 40 deletions(-) create mode 100644 base/cert-manager/scaleway-dns-secret.yaml create mode 100644 base/devtools/penpot-mcp.Dockerfile create mode 100644 base/devtools/penpot-mcp.yaml create mode 100644 base/devtools/penpot-oidc.yaml create mode 100644 base/devtools/penpot.yaml diff --git a/base/cert-manager/kustomization.yaml b/base/cert-manager/kustomization.yaml index 70e54d8..baad087 100644 --- a/base/cert-manager/kustomization.yaml +++ b/base/cert-manager/kustomization.yaml @@ -3,6 +3,7 @@ kind: Kustomization resources: - namespace.yaml + - scaleway-dns-secret.yaml helmCharts: # helm repo add jetstack https://charts.jetstack.io @@ -13,3 +14,11 @@ helmCharts: namespace: cert-manager valuesFile: values.yaml includeCRDs: true + + # helm repo add scaleway https://helm.scw.cloud/ + # DNS-01 webhook for Scaleway DNS — enables wildcard certs. + - name: scaleway-certmanager-webhook + repo: https://helm.scw.cloud/ + version: "0.4.1" + releaseName: scaleway-certmanager-webhook + namespace: cert-manager diff --git a/base/cert-manager/scaleway-dns-secret.yaml b/base/cert-manager/scaleway-dns-secret.yaml new file mode 100644 index 0000000..d90f7bd --- /dev/null +++ b/base/cert-manager/scaleway-dns-secret.yaml @@ -0,0 +1,37 @@ +# Scaleway API credentials for DNS-01 ACME challenges. +# Synced from OpenBao KV path: secret/scaleway-s3 (same API key used for S3 + DNS). +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultAuth +metadata: + name: vso-auth + namespace: cert-manager +spec: + method: kubernetes + mount: kubernetes + kubernetes: + role: vso + serviceAccount: default +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: scaleway-dns-credentials + namespace: cert-manager +spec: + vaultAuthRef: vso-auth + mount: secret + type: kv-v2 + path: scaleway-s3 + refreshAfter: 30s + destination: + name: scaleway-secret + create: true + overwrite: true + transformation: + excludeRaw: true + templates: + SCW_ACCESS_KEY: + text: "{{ index .Secrets \"access-key-id\" }}" + SCW_SECRET_KEY: + text: "{{ index .Secrets \"secret-access-key\" }}" diff --git a/base/data/postgres-cluster.yaml b/base/data/postgres-cluster.yaml index b7133ef..d084552 100644 --- a/base/data/postgres-cluster.yaml +++ b/base/data/postgres-cluster.yaml @@ -44,6 +44,8 @@ spec: - CREATE DATABASE hive_db OWNER hive; - CREATE USER find WITH LOGIN; - CREATE DATABASE find_db OWNER find; + - CREATE USER penpot WITH LOGIN; + - CREATE DATABASE penpot_db OWNER penpot; storage: size: 10Gi diff --git a/base/devtools/kustomization.yaml b/base/devtools/kustomization.yaml index 320b62d..926d4e7 100644 --- a/base/devtools/kustomization.yaml +++ b/base/devtools/kustomization.yaml @@ -9,6 +9,10 @@ resources: - gitea-theme-cm.yaml - gitea-servicemonitor.yaml - gitea-alertrules.yaml + - beam-design.yaml + - penpot.yaml + - penpot-oidc.yaml + - penpot-mcp.yaml helmCharts: # helm repo add gitea-charts https://dl.gitea.com/charts/ diff --git a/base/devtools/penpot-mcp.Dockerfile b/base/devtools/penpot-mcp.Dockerfile new file mode 100644 index 0000000..486b230 --- /dev/null +++ b/base/devtools/penpot-mcp.Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine +RUN npm install -g pnpm@latest @penpot/mcp@latest && \ + cd /usr/local/lib/node_modules/@penpot/mcp && \ + pnpm -r install && \ + pnpm run build +ENV PENPOT_MCP_REMOTE_MODE=true \ + PENPOT_MCP_SERVER_HOST=0.0.0.0 \ + PENPOT_MCP_SERVER_PORT=4401 \ + PENPOT_MCP_WEBSOCKET_PORT=4402 +EXPOSE 4401 4402 +WORKDIR /usr/local/lib/node_modules/@penpot/mcp +CMD ["pnpm", "run", "start"] diff --git a/base/devtools/penpot-mcp.yaml b/base/devtools/penpot-mcp.yaml new file mode 100644 index 0000000..bebee96 --- /dev/null +++ b/base/devtools/penpot-mcp.yaml @@ -0,0 +1,56 @@ +# Penpot MCP server — bridges AI clients to Penpot via the MCP plugin. +# Port 4401: HTTP/SSE for MCP clients (Claude, etc.) +# Port 4402: WebSocket for the Penpot browser plugin +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: penpot-mcp + namespace: devtools +spec: + replicas: 1 + selector: + matchLabels: + app: penpot-mcp + template: + metadata: + labels: + app: penpot-mcp + spec: + containers: + - name: penpot-mcp + image: src.sunbeam.pt/studio/penpot-mcp:latest + ports: + - name: http + containerPort: 4401 + - name: ws + containerPort: 4402 + env: + - name: PENPOT_MCP_REMOTE_MODE + value: "true" + - name: PENPOT_MCP_SERVER_HOST + value: "0.0.0.0" + - name: PENPOT_MCP_SERVER_ADDRESS + value: "mcp-designer.DOMAIN_SUFFIX" + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: penpot-mcp + namespace: devtools +spec: + selector: + app: penpot-mcp + ports: + - name: http + port: 4401 + targetPort: http + - name: ws + port: 4402 + targetPort: ws diff --git a/base/devtools/penpot-oidc.yaml b/base/devtools/penpot-oidc.yaml new file mode 100644 index 0000000..f0601d3 --- /dev/null +++ b/base/devtools/penpot-oidc.yaml @@ -0,0 +1,20 @@ +# Penpot OIDC client — Hydra Maester creates Secret "oidc-penpot" in devtools +# with CLIENT_ID and CLIENT_SECRET keys. +apiVersion: hydra.ory.sh/v1alpha1 +kind: OAuth2Client +metadata: + name: penpot + namespace: devtools +spec: + clientName: Penpot + grantTypes: + - authorization_code + - refresh_token + responseTypes: + - code + scope: openid email profile + redirectUris: + - https://designer.DOMAIN_SUFFIX/api/auth/oidc/callback + tokenEndpointAuthMethod: client_secret_post + secretName: oidc-penpot + skipConsent: true diff --git a/base/devtools/penpot.yaml b/base/devtools/penpot.yaml new file mode 100644 index 0000000..ba0b937 --- /dev/null +++ b/base/devtools/penpot.yaml @@ -0,0 +1,205 @@ +# Penpot — open-source design tool (frontend + backend + exporter). +# OIDC-only auth via Hydra; assets on SeaweedFS; DB on shared CNPG postgres. +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: penpot-config + namespace: devtools +data: + PENPOT_PUBLIC_URI: "https://designer.DOMAIN_SUFFIX" + PENPOT_DATABASE_URI: "postgresql://postgres-rw.data.svc.cluster.local:5432/penpot_db" + PENPOT_DATABASE_USERNAME: "penpot" + PENPOT_REDIS_URI: "redis://valkey.data.svc.cluster.local:6379/3" + PENPOT_ASSETS_STORAGE_BACKEND: "assets-s3" + PENPOT_STORAGE_ASSETS_S3_ENDPOINT: "http://seaweedfs-filer.storage.svc.cluster.local:8333" + PENPOT_STORAGE_ASSETS_S3_BUCKET: "penpot" + PENPOT_OIDC_BASE_URI: "https://auth.DOMAIN_SUFFIX/" + PENPOT_TELEMETRY_ENABLED: "false" + PENPOT_FLAGS: "enable-login-with-oidc disable-login-with-password disable-email-verification disable-registration enable-backend-api-doc enable-auto-file-snapshot enable-tiered-file-data-storage enable-webhooks enable-access-tokens enable-cors" +--- +# ── Frontend (nginx SPA) ───────────────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: penpot-frontend + namespace: devtools +spec: + replicas: 1 + selector: + matchLabels: + app: penpot-frontend + template: + metadata: + labels: + app: penpot-frontend + spec: + containers: + - name: penpot-frontend + image: penpotapp/frontend:latest + ports: + - name: http + containerPort: 8080 + env: + - name: PENPOT_FLAGS + valueFrom: + configMapKeyRef: + name: penpot-config + key: PENPOT_FLAGS + - name: PENPOT_BACKEND_URI + value: "http://penpot-backend:6060" + - name: PENPOT_EXPORTER_URI + value: "http://penpot-exporter:6061" + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: penpot-frontend + namespace: devtools +spec: + selector: + app: penpot-frontend + ports: + - name: http + port: 8080 + targetPort: http +--- +# ── Backend (JVM API + websockets) ─────────────────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: penpot-backend + namespace: devtools +spec: + replicas: 1 + selector: + matchLabels: + app: penpot-backend + template: + metadata: + labels: + app: penpot-backend + spec: + containers: + - name: penpot-backend + image: penpotapp/backend:latest + ports: + - name: http + containerPort: 6060 + envFrom: + - configMapRef: + name: penpot-config + env: + - name: PENPOT_SECRET_KEY + valueFrom: + secretKeyRef: + name: penpot-app-secrets + key: secret-key + - name: PENPOT_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: penpot-db-credentials + key: password + - name: PENPOT_OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: oidc-penpot + key: CLIENT_ID + - name: PENPOT_OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oidc-penpot + key: CLIENT_SECRET + - name: PENPOT_STORAGE_ASSETS_S3_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: penpot-s3-credentials + key: access-key + - name: PENPOT_STORAGE_ASSETS_S3_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: penpot-s3-credentials + key: secret-key + resources: + requests: + cpu: 100m + memory: 512Mi + limits: + memory: 1Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: penpot-backend + namespace: devtools +spec: + selector: + app: penpot-backend + ports: + - name: http + port: 6060 + targetPort: http +--- +# ── Exporter (headless Chromium for PDF/SVG) ───────────────────────────────── +apiVersion: apps/v1 +kind: Deployment +metadata: + name: penpot-exporter + namespace: devtools +spec: + replicas: 1 + selector: + matchLabels: + app: penpot-exporter + template: + metadata: + labels: + app: penpot-exporter + spec: + containers: + - name: penpot-exporter + image: penpotapp/exporter:latest + ports: + - name: http + containerPort: 6061 + env: + - name: PENPOT_SECRET_KEY + valueFrom: + secretKeyRef: + name: penpot-app-secrets + key: secret-key + - name: PENPOT_PUBLIC_URI + valueFrom: + configMapKeyRef: + name: penpot-config + key: PENPOT_PUBLIC_URI + - name: PENPOT_REDIS_URI + valueFrom: + configMapKeyRef: + name: penpot-config + key: PENPOT_REDIS_URI + resources: + requests: + cpu: 50m + memory: 256Mi + limits: + memory: 512Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: penpot-exporter + namespace: devtools +spec: + selector: + app: penpot-exporter + ports: + - name: http + port: 6061 + targetPort: http diff --git a/base/devtools/vault-secrets.yaml b/base/devtools/vault-secrets.yaml index dc03aad..a30f96f 100644 --- a/base/devtools/vault-secrets.yaml +++ b/base/devtools/vault-secrets.yaml @@ -62,6 +62,84 @@ spec: "secret-key": text: "{{ index .Secrets \"secret-key\" }}" --- +# Penpot DB credentials from OpenBao database secrets engine (static role, 24h rotation). +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultDynamicSecret +metadata: + name: penpot-db-credentials + namespace: devtools +spec: + vaultAuthRef: vso-auth + mount: database + path: static-creds/penpot + allowStaticCreds: true + refreshAfter: 5m + rolloutRestartTargets: + - kind: Deployment + name: penpot-backend + destination: + name: penpot-db-credentials + create: true + overwrite: true + transformation: + excludeRaw: true + templates: + password: + text: "{{ index .Secrets \"password\" }}" +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: penpot-s3-credentials + namespace: devtools +spec: + vaultAuthRef: vso-auth + mount: secret + type: kv-v2 + path: seaweedfs + refreshAfter: 30s + rolloutRestartTargets: + - kind: Deployment + name: penpot-backend + destination: + name: penpot-s3-credentials + create: true + overwrite: true + transformation: + excludeRaw: true + templates: + "access-key": + text: "{{ index .Secrets \"access-key\" }}" + "secret-key": + text: "{{ index .Secrets \"secret-key\" }}" +--- +# Penpot app secrets (secret-key for internal auth between backend/exporter). +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: penpot-app-secrets + namespace: devtools +spec: + vaultAuthRef: vso-auth + mount: secret + type: kv-v2 + path: penpot + refreshAfter: 30s + rolloutRestartTargets: + - kind: Deployment + name: penpot-backend + - kind: Deployment + name: penpot-exporter + destination: + name: penpot-app-secrets + create: true + overwrite: true + transformation: + excludeRaw: true + templates: + "secret-key": + text: "{{ index .Secrets \"secret-key\" }}" +--- apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultStaticSecret metadata: diff --git a/base/ingress/pingora-config.yaml b/base/ingress/pingora-config.yaml index d2f534b..9bbb225 100644 --- a/base/ingress/pingora-config.yaml +++ b/base/ingress/pingora-config.yaml @@ -172,6 +172,29 @@ data: host_prefix = "find" backend = "http://find-backend.lasuite.svc.cluster.local:8000" + [[routes]] + host_prefix = "design" + backend = "http://beam-design.devtools.svc.cluster.local:80" + + [[routes]] + host_prefix = "designer" + backend = "http://penpot-frontend.devtools.svc.cluster.local:8080" + websocket = true + + [[routes]] + host_prefix = "mcp-designer" + backend = "http://penpot-mcp.devtools.svc.cluster.local:4401" + websocket = true + + [[routes.paths]] + prefix = "/" + backend = "http://penpot-mcp.devtools.svc.cluster.local:4401" + auth_request = "http://hydra-public.ory.svc.cluster.local:4444/userinfo" + + [[routes.paths]] + prefix = "/ws" + backend = "http://penpot-mcp.devtools.svc.cluster.local:4402" + [[routes]] host_prefix = "src" backend = "http://gitea-http.devtools.svc.cluster.local:3000" diff --git a/base/ory/hydra-values.yaml b/base/ory/hydra-values.yaml index 636b474..b1993d7 100644 --- a/base/ory/hydra-values.yaml +++ b/base/ory/hydra-values.yaml @@ -61,6 +61,7 @@ hydra-maester: - lasuite - matrix - monitoring + - devtools # ServiceMonitor created as standalone resource (hydra-servicemonitor.yaml) — # chart's built-in ServiceMonitor requires .Capabilities.APIVersions which diff --git a/overlays/production/cert-manager.yaml b/overlays/production/cert-manager.yaml index 70519a0..6e9c10b 100644 --- a/overlays/production/cert-manager.yaml +++ b/overlays/production/cert-manager.yaml @@ -1,9 +1,7 @@ -# cert-manager issuers and certificate for production TLS. +# cert-manager issuers and wildcard certificate for production TLS. # -# WORKFLOW: start with letsencrypt-staging to verify the HTTP-01 challenge -# flow works without burning production rate limits. Once the staging cert -# is issued successfully, flip the Certificate issuerRef to letsencrypt-production -# and delete the old Secret so cert-manager re-issues with a trusted cert. +# Uses DNS-01 via Scaleway DNS webhook for wildcard support. +# No more per-subdomain SANs — *.DOMAIN_SUFFIX covers everything. # # ACME_EMAIL is substituted by sunbeam apply. --- @@ -19,12 +17,19 @@ spec: privateKeySecretRef: name: letsencrypt-staging-account-key solvers: - - http01: - ingress: - serviceType: ClusterIP + - dns01: + webhook: + groupName: acme.scaleway.com + solverName: scaleway + config: + accessKeySecretRef: + key: SCW_ACCESS_KEY + name: scaleway-secret + secretKeySecretRef: + key: SCW_SECRET_KEY + name: scaleway-secret --- # Let's Encrypt production — trusted cert, strict rate limits. -# Switch to this once staging confirms challenges resolve correctly. apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: @@ -36,13 +41,19 @@ spec: privateKeySecretRef: name: letsencrypt-production-account-key solvers: - - http01: - ingress: - serviceType: ClusterIP + - dns01: + webhook: + groupName: acme.scaleway.com + solverName: scaleway + config: + accessKeySecretRef: + key: SCW_ACCESS_KEY + name: scaleway-secret + secretKeySecretRef: + key: SCW_SECRET_KEY + name: scaleway-secret --- -# Certificate covering all proxy subdomains. -# Start with letsencrypt-staging. Once verified, change issuerRef.name to -# letsencrypt-production and delete the pingora-tls Secret to force re-issue. +# Wildcard certificate covering all subdomains. apiVersion: cert-manager.io/v1 kind: Certificate metadata: @@ -55,27 +66,4 @@ spec: kind: ClusterIssuer dnsNames: - DOMAIN_SUFFIX - - docs.DOMAIN_SUFFIX - - meet.DOMAIN_SUFFIX - - drive.DOMAIN_SUFFIX - - mail.DOMAIN_SUFFIX - - messages.DOMAIN_SUFFIX - - people.DOMAIN_SUFFIX - - src.DOMAIN_SUFFIX - - auth.DOMAIN_SUFFIX - - s3.DOMAIN_SUFFIX - - metrics.DOMAIN_SUFFIX - - systemmetrics.DOMAIN_SUFFIX - - systemlogs.DOMAIN_SUFFIX - - systemtracing.DOMAIN_SUFFIX - - admin.DOMAIN_SUFFIX - - integration.DOMAIN_SUFFIX - - livekit.DOMAIN_SUFFIX - - cal.DOMAIN_SUFFIX - - projects.DOMAIN_SUFFIX - - id.DOMAIN_SUFFIX - - hydra.DOMAIN_SUFFIX - - search.DOMAIN_SUFFIX - - vault.DOMAIN_SUFFIX - - find.DOMAIN_SUFFIX - - call.DOMAIN_SUFFIX + - "*.DOMAIN_SUFFIX" diff --git a/scripts/local-seed-secrets.sh b/scripts/local-seed-secrets.sh index c29c567..bfb16c5 100755 --- a/scripts/local-seed-secrets.sh +++ b/scripts/local-seed-secrets.sh @@ -58,7 +58,7 @@ done echo "==> Setting postgres user passwords..." PG_POD=$(kubectl $CTX -n data get pods -l cnpg.io/cluster=postgres,role=primary -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") if [[ -n "$PG_POD" ]]; then - for user in kratos hydra gitea hive docs meet drive messages conversations people find; do + for user in kratos hydra gitea hive docs meet drive messages conversations people find penpot; do kubectl $CTX -n data exec "$PG_POD" -c postgres -- \ psql -U postgres -c "ALTER USER $user WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true done @@ -94,6 +94,16 @@ create_secret devtools gitea-s3-credentials \ --from-literal=access-key="$S3_ACCESS_KEY" \ --from-literal=secret-key="$S3_SECRET_KEY" +create_secret devtools penpot-db-credentials \ + --from-literal=password="$DB_PASSWORD" + +create_secret devtools penpot-s3-credentials \ + --from-literal=access-key="$S3_ACCESS_KEY" \ + --from-literal=secret-key="$S3_SECRET_KEY" + +create_secret devtools penpot-app-secrets \ + --from-literal=secret-key="penpot-local-secret-key-not-for-production" + # Storage namespace ensure_ns storage create_secret storage seaweedfs-s3-credentials \ @@ -181,6 +191,7 @@ else bao kv put secret/seaweedfs access-key='$S3_ACCESS_KEY' secret-key='$S3_SECRET_KEY' bao kv put secret/hive db-url='postgresql://hive:${DB_PASSWORD}@postgres-rw.data.svc.cluster.local:5432/hive_db' oidc-client-id='hive-local' oidc-client-secret='hive-local-secret' bao kv put secret/people db-password='$DB_PASSWORD' django-secret-key='local-dev-people-django-secret-key-not-for-production' + bao kv put secret/penpot db-password='$DB_PASSWORD' secret-key='penpot-local-secret-key-not-for-production' bao kv put secret/livekit api-key='$LIVEKIT_API_KEY' api-secret='$LIVEKIT_API_SECRET' " 2>/dev/null echo " Done."