From 1646ee1bc3dada46b40fa7a269ece1a90c7fe67b Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Wed, 14 Oct 2026 22:15:38 +0200 Subject: [PATCH] feat: deploy to Frostmoln production The AWS deploy-prod job has failed since CI left AWS (no k8s.unbound.se context on the runners). Deploys now go to the Frostmoln production cluster (namespace schemas), the way the sites do. Frostmoln enforces restricted Pod Security: the pod runs as the image's non-root user with a matching securityContext. The prod-only manifests move to the prod-frostmoln target; the ALB ingress becomes a Traefik one with a Let's Encrypt certificate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ma8eHhW59hLAJuLzBnLvUu --- .gitea/workflows/ci.yaml | 22 +++++++++++++--- Dockerfile | 3 ++- ...rod.yaml => autoscale-prod-frostmoln.yaml} | 0 k8s/deploy.yaml | 18 +++++++++---- k8s/ingress-prod-frostmoln.yaml | 21 ++++++++++++++++ k8s/ingress-prod.yaml | 25 ------------------- ...{pdb-prod.yaml => pdb-prod-frostmoln.yaml} | 0 nginx/nginx.conf | 12 ++++++--- 8 files changed, 63 insertions(+), 38 deletions(-) rename k8s/{autoscale-prod.yaml => autoscale-prod-frostmoln.yaml} (100%) create mode 100644 k8s/ingress-prod-frostmoln.yaml delete mode 100644 k8s/ingress-prod.yaml rename k8s/{pdb-prod.yaml => pdb-prod-frostmoln.yaml} (100%) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 59de4b4..b56da31 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -18,16 +18,30 @@ jobs: - name: Build and push run: unset GITEA_TOKEN && build && push - deploy-prod: + deploy-prod-frostmoln: needs: build if: gitea.ref == 'refs/heads/main' runs-on: ubuntu-latest env: - BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_CONTENT }} + BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_FROSTMOLN_PRODUCTION }} GITEA_REPOSITORY: ${{ gitea.repository }} + FM_TENANT: 9ae61342-0f76-476f-8176-e46f6b0b0c32 environment: prod steps: - uses: actions/checkout@v7 - uses: buildtool/setup-buildtools-action@v1 - - name: Deploy to production - run: deploy prod + - name: Install fm + run: | + sudo install -d -m 0755 /etc/apt/keyrings + sudo curl -fsSL -o /etc/apt/keyrings/frostmoln.gpg https://get.frostmoln.cloud/deb/frostmoln.gpg + sudo curl -fsSL -o /etc/apt/sources.list.d/frostmoln.sources https://get.frostmoln.cloud/deb/frostmoln.sources + sudo apt-get update && sudo apt-get install -y fm + - name: Deploy to Frostmoln production + env: + FROSTMOLN_API_KEY: ${{ secrets.FROSTMOLN_PRODUCTION_DEPLOY_KEY }} + # The cluster API is private: the kubeconfig's exec plugin opens an fm + # tunnel on demand. fm reads the key from its config file, not the env. + run: | + fm auth api-key set "$FROSTMOLN_API_KEY" > /dev/null + fm kubernetes cluster kubeconfig ${{ vars.FROSTMOLN_PRODUCTION_CLUSTER_ID }} --exec --file "$RUNNER_TEMP/kubeconfig" + KUBECONFIG_CONTENT="$(cat "$RUNNER_TEMP/kubeconfig")" deploy prod-frostmoln -n schemas diff --git a/Dockerfile b/Dockerfile index af922aa..5c25c65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,8 @@ RUN npm run postinstall && npm run generate && npm run lint && npm run test:cove FROM scratch as export COPY --from=builder /build/coverage / -FROM amd64/nginx:1.31.7@sha256:227f31dac037a2f1cdbd17ca9eec940a76c6c1b3187f5285eca2c3edb5d19c31 +# Unprivileged (uid 101, port 8080): Frostmoln enforces restricted Pod Security. +FROM nginxinc/nginx-unprivileged:1.31.6@sha256:1d26a516fb93b18f9c0214dc9c645054486e516f13ff09ef63d5bd5c622278a2 MAINTAINER Joakim Olsson COPY ./nginx/nginx.conf /etc/nginx/nginx.conf diff --git a/k8s/autoscale-prod.yaml b/k8s/autoscale-prod-frostmoln.yaml similarity index 100% rename from k8s/autoscale-prod.yaml rename to k8s/autoscale-prod-frostmoln.yaml diff --git a/k8s/deploy.yaml b/k8s/deploy.yaml index 07b5f05..de7d070 100644 --- a/k8s/deploy.yaml +++ b/k8s/deploy.yaml @@ -23,6 +23,11 @@ spec: app.kubernetes.io/name: schemas-app app.kubernetes.io/instance: schemas spec: + securityContext: + runAsNonRoot: true + runAsUser: 101 + seccompProfile: + type: RuntimeDefault affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: @@ -39,19 +44,23 @@ spec: - name: schemas-app image: ${IMAGE} imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] ports: - - containerPort: 80 + - containerPort: 8080 readinessProbe: httpGet: path: / - port: 80 + port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 5 livenessProbe: httpGet: path: / - port: 80 + port: 8080 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 @@ -71,9 +80,8 @@ metadata: labels: app.kubernetes.io/name: schemas-app spec: - type: NodePort selector: app.kubernetes.io/name: schemas-app ports: - port: 80 - targetPort: 80 + targetPort: 8080 diff --git a/k8s/ingress-prod-frostmoln.yaml b/k8s/ingress-prod-frostmoln.yaml new file mode 100644 index 0000000..bb38a9e --- /dev/null +++ b/k8s/ingress-prod-frostmoln.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: schemas-app + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt +spec: + ingressClassName: traefik + rules: + - host: "schemas.unbound.se" + http: + paths: + # Everything but /query, which the schemas API's longer path takes. + - path: / + pathType: Prefix + backend: + service: + name: schemas-app + port: + number: 80 diff --git a/k8s/ingress-prod.yaml b/k8s/ingress-prod.yaml deleted file mode 100644 index 4b056a7..0000000 --- a/k8s/ingress-prod.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: schemas-app-ingress - annotations: - alb.ingress.kubernetes.io/group.name: "default" - alb.ingress.kubernetes.io/group.order: "15" - alb.ingress.kubernetes.io/scheme: internet-facing - alb.ingress.kubernetes.io/target-type: instance - alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80},{"HTTPS": 443}]' - alb.ingress.kubernetes.io/ssl-redirect: "443" - alb.ingress.kubernetes.io/healthcheck-path: '/' -spec: - ingressClassName: "alb" - rules: - - host: "schemas.unbound.se" - http: - paths: - - path: /* - pathType: ImplementationSpecific - backend: - service: - name: schemas-app - port: - number: 80 diff --git a/k8s/pdb-prod.yaml b/k8s/pdb-prod-frostmoln.yaml similarity index 100% rename from k8s/pdb-prod.yaml rename to k8s/pdb-prod-frostmoln.yaml diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 82fc3f7..b3f022e 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,14 +1,20 @@ -user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; -pid /var/run/nginx.pid; +pid /tmp/nginx.pid; events { worker_connections 1024; } http { + # Writable by the unprivileged (uid 101) worker. + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + include /etc/nginx/mime.types; default_type application/octet-stream; types { @@ -22,7 +28,7 @@ http { keepalive_timeout 65; server { - listen 80; + listen 8080; server_name localhost; root /usr/share/nginx/html; index index.html index.htm;