diff --git a/.gitignore b/.gitignore index 093f86bf..c7f7379f 100644 --- a/.gitignore +++ b/.gitignore @@ -59,8 +59,8 @@ next-env.d.ts # Sibling repos /repos/ -# k8s deploy secret (create from deploy/k8s/secret.example.yaml) -/deploy/k8s/secret.yaml +# k8s deploy secrets (create from the matching overlay's secret.example.yaml) +/deploy/k8s/overlays/*/secret.yaml # S/MIME plugin build output (rebuild with: cd vnc/plugins/smime && npm run build) vnc/plugins/smime/node_modules/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..5e356e55 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,165 @@ +# GitLab-CI dev→prod pipeline for VNCmail+ — GitOps via ArgoCD. +# +# Revised after direct inspection of the real infrastructure found ArgoCD +# already installed (idle, zero Applications) on the dev-k8s-1/2/3 cluster. +# That's more idiomatic than a runner-executes-kubectl design, and it means +# this pipeline needs ZERO cluster credentials — CI only ever talks to the +# container registry and to this git repo. ArgoCD (which already has +# whatever cluster access it needs, set up once when its Applications were +# registered — see deploy/argocd/) is what actually applies anything. +# +# Design: +# - One image name, environment lives only in the tag. No more -dev/-beta +# name confusion from the old GitHub Actions workflow. +# - MR into `dev`: verify only (typecheck/lint/unit test/build check). No +# push, no deploy — this is the multi-developer merge gate. +# - Push to `dev`: build+push an immutable `sha-` tag, then commit a +# one-line tag-bump into overlays/dev/image-tag/kustomization.yaml +# (`[skip ci]`, so this doesn't retrigger itself). ArgoCD's `vncmail-dev` +# Application has automated sync — it notices the git change and applies +# it. No approval needed, dev always deploys, and this job never touches +# the cluster directly. +# - Push to `main`: NEVER rebuilds. `main` only ever advances via +# `git merge --ff-only dev`, so main's HEAD commit already has a built +# image (the same sha- tag dev already deployed). This job just bumps +# overlays/prod/image-tag/kustomization.yaml to point at that same tag. +# The actual promotion gate is a HUMAN clicking Sync on the `vncmail-prod` ArgoCD +# Application (deliberately NOT automated sync) — not a GitLab manual +# job, since ArgoCD already provides that exact gate more directly. +# Until prod Stalwart/hostname/secrets are real (see VNCMAIL-SETUP.md), +# nobody should click that Sync button — but nothing here does it for +# you either way. +# +# Deliberately single-platform (linux/amd64) — this pipeline serves two +# known amd64 microk8s clusters, not public multi-arch distribution (that's +# what the GHCR release workflows are for, untouched by this file). +# +# Prerequisite this file assumes (documented in VNCMAIL-SETUP.md, not +# something this file can set up itself): +# - GitLab Container Registry enabled for this project (confirmed done). +# - A GitLab Runner (any kind — no cluster access needed at all now). +# - Either "allow this job token to push to this project" enabled +# (Settings → CI/CD → Job token permissions), OR a project access token +# with `write_repository` scope in $GITLAB_PUSH_TOKEN. The job below +# tries CI_JOB_TOKEN first (see the script). +# +# deploy/k8s/ca/ (the EJBCA internal CA) is never referenced anywhere below, +# and neither ArgoCD Application in deploy/argocd/ points at it — that stays +# a fully manual, human-only runbook (see deploy/k8s/ca/README.md). + +stages: + - verify + - build + - bump-dev + - bump-prod + +variables: + IMAGE: $CI_REGISTRY_IMAGE/vncmail-plus + GIT_STRATEGY: clone + +# --------------------------------------------------------------------------- +# verify — required check on every MR into dev. No registry, no cluster. +# --------------------------------------------------------------------------- +verify: + stage: verify + image: node:24-alpine + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + script: + - npm ci + - npm run typecheck + - npm run lint + - npm run test:translations + - npm run build + # test:integration is deliberately NOT here — it spins up a real Stalwart + # fixture via docker-compose (Docker-in-Docker), heavier than a fast MR + # gate should be. Candidate for a separate scheduled job, not a blocker. + +# --------------------------------------------------------------------------- +# build — push to dev only. Builds once; main never rebuilds (see header). +# --------------------------------------------------------------------------- +build: + stage: build + image: docker:27-cli + services: + - docker:27-dind + rules: + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"' + before_script: + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin + script: + - docker build --build-arg GIT_COMMIT=$CI_COMMIT_SHA -t "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" -t "$IMAGE:dev-latest" . + - docker push "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" + - docker push "$IMAGE:dev-latest" + +# --------------------------------------------------------------------------- +# bump-dev — no cluster access. Commits the just-built tag into the overlay +# ArgoCD watches; ArgoCD's automated sync does the actual apply. +# --------------------------------------------------------------------------- +bump-dev: + stage: bump-dev + image: alpine/git:2.47.0 + rules: + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"' + script: + - TAG="sha-$CI_COMMIT_SHORT_SHA" + - | + cat > deploy/k8s/overlays/dev/image-tag/kustomization.yaml < deploy/k8s/overlays/prod/image-tag/kustomization.yaml <`, then `bump-dev` + commits that tag into `deploy/k8s/overlays/dev/image-tag/kustomization.yaml` + (`[skip ci]`). ArgoCD's `vncmail-dev` Application picks up the git change. +3. **Merge to `main`** (fast-forward only, see below) → `bump-prod` points + `overlays/prod/image-tag/` at that same tag — **no rebuild**. The actual + promotion gate is a **human clicking Sync** on the `vncmail-prod` ArgoCD + Application, which is permanently manual-sync (never automated) — that's + the Vercel-style "Promote to Production" button, just living in ArgoCD's + UI instead of GitLab's. + +### What's left to wire up (one-time, human steps) + +1. **Add the ArgoCD deploy key to GitLab** — Project → Settings → Repository + → Deploy keys → add (read-only is enough): + ``` + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOURjX/Y9zfB785DyLEF1GUq4HhWujrqeXag8oxdMciq argocd@dev-k8s (vncmail-plus read-only) + ``` + Until this is added, `vncmail-dev`'s ArgoCD Application (already created, + `kubectl -n argocd get application vncmail-dev`) shows a benign + `ComparisonError` (SSH handshake failing) — expected, not a bug. +2. **Let CI push tag-bumps back to this repo** — either enable "this project + can be accessed by CI/CD job tokens from other projects" → actually + simpler: Settings → CI/CD → Job token permissions → allow this project's + own job token to push to itself, OR create a Project Access Token + (`write_repository` scope) and add it as a masked CI/CD variable + `GITLAB_PUSH_TOKEN` (the pipeline tries that first, falls back to + `CI_JOB_TOKEN`). +3. **One-time namespace bootstrap** (CI/ArgoCD deliberately never manage + secret contents — see `deploy/k8s/README.md` §3): + ```bash + # against dev-k8s (ArgoCD's CreateNamespace=true will make `vncmail` on + # first sync, or create it yourself first — either order works) + kubectl create secret docker-registry ghcr-pull -n vncmail ... # or make the GHCR package public + cp deploy/k8s/overlays/dev/secret.example.yaml secret.yaml # edit SESSION_SECRET + kubectl apply -f secret.yaml + ``` +4. **First sync** — ArgoCD UI at `https://argo.devcluster.vnc.de` + (username `admin`, password: `kubectl -n argocd get secret + argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d` + — rotate it after logging in once) → `vncmail-dev` → Sync. Once that's + clean, flip `deploy/argocd/vncmail-dev-app.yaml`'s commented-out + `automated:` block on and re-apply, so dev auto-syncs on every push from + then on. +5. **Production** (later, deliberately not wired yet): decide a real + hostname, stand up prod Stalwart, register `node1-3` as an ArgoCD-managed + cluster, apply `deploy/argocd/vncmail-prod-app.yaml`, fill in real + `overlays/prod` values, create a real ClusterIssuer on `node1-3` (there + isn't one today), then click Sync once — deliberately not before. + +Historical note: the old `-dev`/`-beta` GHCR image-name split +(`.github/workflows/docker-publish.yml`) is retired by this — one image name +now, environment lives only in the tag. + ## Deploy (Kubernetes / microk8s) Full runbook: **[deploy/k8s/README.md](deploy/k8s/README.md)**. In short: -1. CI builds the image on push to `dev`/`main` → `ghcr.io/brvncde-dotcom/vncmail-plus-dev` (`.github/workflows/docker-publish.yml`). -2. `kubectl apply` the manifests in `deploy/k8s/` (namespace, 4 PVCs, deployment, service, ingress) + a `secret.yaml` (from `secret.example.yaml`) + a `ghcr-pull` image-pull secret. -3. Point `vncmail.sandbox.vnc.de` DNS at the ingress; cert-manager issues TLS. +1. CI (above) builds and pushes the image, one name/many tags, to GitLab's + registry. +2. `kubectl apply -k deploy/k8s/overlays/dev` (or `overlays/prod`, once real) + — base manifests (namespace, 4 PVCs, deployment, service, ingress) live in + `deploy/k8s/base/`, environment differences (namespace, hostname, replica + count) are overlay patches. +3. DNS + a `secret.yaml` (from the overlay's `secret.example.yaml`, gitignored, + created once by hand — CI never manages secret contents) + an image-pull + secret are the remaining manual, human, one-time steps per environment. Runs alongside the existing `bulwark.sandbox.vnc.de`. Match your cluster's StorageClass / IngressClass / cert issuer to bulwark's (see the runbook). ## Deploy workflow (dev-first — ALWAYS) -Same flow as every other VNC/SRC repo: +Same flow as every other VNC/SRC repo, now enforced structurally by CI rather +than by convention: -1. Work on `dev` (or `vnc/*` → PR into `dev`). Push to `dev` → CI builds the `-dev` image → `kubectl -n vncmail rollout restart deploy/vncmail-plus` to pull it. QA at `vncmail.sandbox.vnc.de`. +1. Work on `dev` (or `vnc/*` → MR into `dev`, CI-gated). Merge → auto-builds + and auto-deploys to `vncmail.sandbox.vnc.de`. QA there. 2. **Promote to production only on explicit go-live** — merge `dev` → `main`: ```bash git log dev..main # MUST be empty — main must have nothing dev lacks (else prod would revert) git checkout main && git merge --ff-only dev - git push origin main # CI builds the production image + git push gitlab main # never GitHub — opens the manual `promote` job, does not run it git checkout dev ``` - Then roll the production deployment to the new image (pin its digest — see deploy/k8s/README.md). - Never push straight to `main`. Never let a dev→main merge silently revert prod. + Then click `promote` in the GitLab pipeline UI (protected `production` + environment — requires the right role) once prod actually exists (see + "CI/CD" above). Never push straight to `main`. Never let a dev→main merge + silently revert prod. ## Syncing upstream (Bulwark releases) diff --git a/deploy/argocd/vncmail-dev-app.yaml b/deploy/argocd/vncmail-dev-app.yaml new file mode 100644 index 00000000..dc1d635c --- /dev/null +++ b/deploy/argocd/vncmail-dev-app.yaml @@ -0,0 +1,29 @@ +# Registered on the dev-k8s-1/2/3 cluster (where ArgoCD already lives) via +# `kubectl apply` directly to the argocd namespace — this file is the +# version-controlled record of that, not something ArgoCD itself syncs +# (no app-of-apps here, deliberately kept simple for two Applications). +# +# syncPolicy starts WITHOUT automated — manual sync until the one-time +# per-namespace bootstrap (vncmail-env secret, image-pull secret — see +# deploy/k8s/README.md §3) is done by hand once. Flip to automated (see +# commented block below) only after a first manual sync succeeds cleanly. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vncmail-dev + namespace: argocd +spec: + project: default + source: + repoURL: git@gitlab.vnc.biz:gitlab-instance-b9b5cf2f/vncmail-plus.git + targetRevision: dev + path: deploy/k8s/overlays/dev + destination: + server: https://kubernetes.default.svc # in-cluster — ArgoCD and vncmail-dev share this cluster + namespace: vncmail + syncPolicy: + syncOptions: + - CreateNamespace=true + # automated: + # prune: true + # selfHeal: true diff --git a/deploy/argocd/vncmail-prod-app.yaml b/deploy/argocd/vncmail-prod-app.yaml new file mode 100644 index 00000000..4c1164b8 --- /dev/null +++ b/deploy/argocd/vncmail-prod-app.yaml @@ -0,0 +1,30 @@ +# NOT YET APPLIED to any cluster. Scaffolding only, matching +# deploy/k8s/overlays/prod's own "inert until Phase D" status. +# +# Unlike vncmail-dev-app.yaml, this targets a DIFFERENT cluster (node1-3, +# the HA "prod" cluster) than the one ArgoCD itself runs on (dev-k8s). +# That means before this can be applied, node1-3 needs to be registered as +# an ArgoCD-managed cluster (`argocd cluster add`, or an equivalent +# ServiceAccount+kubeconfig secret) — deliberately not done yet: there's no +# reason to wire cross-cluster RBAC into the prod HA cluster before prod +# hostname/Stalwart/secrets are real and someone's actually promoting. +# +# syncPolicy has no automated block at all, and won't get one even later — +# prod stays manual-sync-only permanently. That's the promotion gate. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vncmail-prod + namespace: argocd +spec: + project: default + source: + repoURL: git@gitlab.vnc.biz:gitlab-instance-b9b5cf2f/vncmail-plus.git + targetRevision: main + path: deploy/k8s/overlays/prod + destination: + server: CHANGEME # the node1-3 cluster's registered ArgoCD server URL, once added + namespace: vncmail-prod + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/deploy/k8s/README.md b/deploy/k8s/README.md index 697cc771..b4325fa1 100644 --- a/deploy/k8s/README.md +++ b/deploy/k8s/README.md @@ -1,29 +1,68 @@ # VNCmail+ — Admin Deployment Guide (microk8s) Deploy VNCmail+ (VNC's Bulwark fork) as a container at **`vncmail.sandbox.vnc.de`**, -**alongside** the existing `bulwark.sandbox.vnc.de`. Plain `kubectl apply` — no -GitOps needed. +**alongside** the existing `bulwark.sandbox.vnc.de`. > Why a container (not Vercel): Bulwark is stateful — it writes settings/admin/ > telemetry to `/app/data`, which needs persistent volumes. +## Structure — base + overlays + +``` +deploy/k8s/ + base/ # shared manifest shapes (namespace-agnostic) + overlays/ + dev/ # the live sandbox — vncmail.sandbox.vnc.de, namespace vncmail + prod/ # scaffolded, NOT YET LIVE — see "Production status" below + ca/ # separate, isolated EJBCA internal CA — see ca/README.md. + # Never composed with base/ or either overlay above. +``` + +`kubectl apply -k overlays/dev` (or `overlays/prod`, once real) instead of +applying `base/` directly — `base/` alone has no namespace and won't apply +meaningfully on its own. + +## Routine deploys go through CI + ArgoCD now + +As of the GitLab CI/CD pipeline (`.gitlab-ci.yml`, see `../../VNCMAIL-SETUP.md` +§ CI/CD), **pushing to `dev` auto-builds and bumps the deploy tag; ArgoCD's +`vncmail-dev` Application applies it** — you should not normally need to run +`kubectl apply` for the sandbox by hand anymore, and CI never touches the +cluster directly (it only ever talks to the registry and to this git repo). +This guide's manual steps below are for first-time setup, the one-time +secret creation CI/ArgoCD deliberately never automate, and troubleshooting. + +## Production status + +**There is no production VNCmail+ deployment yet.** `overlays/prod/` exists +in the repo but is inert: its ingress hostname and its secret's +`JMAP_SERVER_URL` are both obvious placeholders (`vncmail.CHANGEME.invalid` / +`https://REPLACE-ME-prod-stalwart-not-yet-deployed.invalid`) that will fail +loudly rather than silently deploy against the wrong backend. Applying it +requires, in order: a real prod Stalwart instance to exist, a real hostname +decision, DNS, a real `secret.yaml`, and the `.gitlab-ci.yml` `promote` job's +`kubectl apply` step (currently a TODO placeholder) filled in. None of that +is CI's job to decide — it's an explicit, human-triggered event. + --- -## 1. What you are deploying +## 1. What you are deploying (per overlay) | # | Object | File | Purpose | |---|--------|------|---------| -| 1 | Namespace `vncmail` | `namespace.yaml` | Isolates the app | -| 2 | 4× PersistentVolumeClaim | `pvc.yaml` | `/app/data/{settings,admin,admin-state,telemetry}` | -| 3 | Secret `vncmail-env` | `secret.yaml` *(you create it)* | App config (JMAP URL, session secret, branding) | -| 4 | Secret `ghcr-pull` | *(you create it — command below)* | Pull the private image from GHCR | -| 5 | Deployment `vncmail-plus` | `deployment.yaml` | The app pod | -| 6 | Service `vncmail-plus` | `service.yaml` | ClusterIP :80 → pod :3000 | -| 7 | Ingress `vncmail-plus` | `ingress.yaml` | TLS host `vncmail.sandbox.vnc.de` | +| 1 | Namespace | `overlays//namespace.yaml` | Isolates the app (`vncmail` for dev, `vncmail-prod` for prod) | +| 2 | 4× PersistentVolumeClaim | `base/pvc.yaml` | `/app/data/{settings,admin,admin-state,telemetry}` | +| 3 | Secret `vncmail-env` | `overlays//secret.yaml` *(you create it)* | App config (JMAP URL, session secret, branding) | +| 4 | Image-pull secret | *(you create it — command below)* | Pull the (currently private) image | +| 5 | Deployment `vncmail-plus` | `base/deployment.yaml` (+ overlay patches) | The app pod | +| 6 | Service `vncmail-plus` | `base/service.yaml` | ClusterIP :80 → pod :3000 | +| 7 | Ingress `vncmail-plus` | `base/ingress.yaml` (+ overlay patches for prod) | TLS host | -**Image:** `ghcr.io/brvncde-dotcom/vncmail-plus-dev:latest` -(built automatically by CI from the `dev` branch). For anything beyond the -sandbox, pin a digest — see §5. +**Image:** CI builds and pushes to `registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus` +(tag `sha-` per deploy, moving pointers `dev-latest`/`prod-latest`). The +`ghcr.io/brvncde-dotcom/vncmail-plus-dev` image referenced in `base/deployment.yaml` +is a legacy default only — CI overrides it per-deploy via `kubectl set image`, +so what's committed there never needs to track what's actually running. --- @@ -43,45 +82,48 @@ kubectl get ingressclass kubectl get clusterissuer # cert-manager issuers (if used) ``` -Then edit if they differ from the defaults below: +Then edit if they differ from the defaults below (in `base/`, so both overlays +pick up the fix): | Value | Default in manifests | File to edit | |-------|----------------------|--------------| -| StorageClass | `microk8s-hostpath` | `pvc.yaml` (all 4) | -| IngressClass | `public` | `ingress.yaml` | -| cert-manager issuer | `letsencrypt-prod` | `ingress.yaml` | +| StorageClass | `microk8s-hostpath` | `base/pvc.yaml` (all 4) | +| IngressClass | `public` | `base/ingress.yaml` | +| cert-manager issuer | `letsencrypt-prod` | `base/ingress.yaml` | --- -## 3. Deploy (copy-paste, in order) +## 3. First-time setup (one-time, per environment — CI never does this) ```bash -cd deploy/k8s +cd deploy/k8s/overlays/dev # or overlays/prod, once real -# a) Namespace -kubectl apply -f namespace.yaml - -# b) Image-pull secret — the GHCR package is private. -# Use a GitHub PAT (classic) with the read:packages scope. +# a) Image-pull secret — the registry package is private. kubectl create secret docker-registry ghcr-pull \ --namespace vncmail \ --docker-server=ghcr.io \ --docker-username=brvncde-dotcom \ --docker-password='' \ --docker-email=br@vnc.biz +# Once CI has cut over to registry.gitlab.vnc.biz, this becomes a +# docker-registry secret for that registry instead — see VNCMAIL-SETUP.md. -# c) App config secret — copy the template, set a real SESSION_SECRET, apply. +# b) App config secret — copy the template, set a real SESSION_SECRET, apply. cp secret.example.yaml secret.yaml # edit secret.yaml: SESSION_SECRET: "$(openssl rand -base64 32)" kubectl apply -f secret.yaml -# d) Everything else (PVCs, Deployment, Service, Ingress) +# c) Everything else (namespace, PVCs, Deployment, Service, Ingress) kubectl apply -k . ``` -> Alternative to (b): make the GHCR package public -> (GitHub → Packages → vncmail-plus-dev → Package settings → Change visibility), -> then delete the `imagePullSecrets:` block from `deployment.yaml`. +> Alternative to (a): make the registry package public, then delete the +> `imagePullSecrets:` block from `base/deployment.yaml`. + +After this one-time setup, routine deploys to `dev` happen automatically via +CI on every push — see "Routine deploys go through CI now" above. This +section is for first-time bring-up (or `overlays/prod`, once it's real) and +troubleshooting, not the everyday path. --- @@ -104,16 +146,23 @@ a bare username. ## 5. Update to a new build -```bash -# CI rebuilds ghcr.io/brvncde-dotcom/vncmail-plus-dev on every push to `dev`. -kubectl -n vncmail rollout restart deploy/vncmail-plus # pulls :latest (imagePullPolicy: Always) +Normally you don't — CI's `bump-dev` job + ArgoCD's automated sync do this +on every push to `dev`. To do it by hand (e.g. troubleshooting, before +automated sync is turned on): -# Production: pin a digest instead of :latest so rollouts are deterministic. +```bash kubectl -n vncmail set image deploy/vncmail-plus \ - vncmail-plus=ghcr.io/brvncde-dotcom/vncmail-plus-dev@sha256: + vncmail-plus=registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus:sha- ``` -Rollback: `kubectl -n vncmail rollout undo deploy/vncmail-plus` +ArgoCD will overwrite this on its next sync unless you also update +`deploy/k8s/overlays/dev/image-tag/kustomization.yaml` to match — that file +is CI-owned (see its header comment), so a by-hand `set image` is only ever +a temporary override, not a real fix. + +Rollback (bypassing ArgoCD temporarily): `kubectl -n vncmail rollout undo deploy/vncmail-plus`. +The real rollback is reverting the commit that bumped the tag and letting +ArgoCD re-sync. --- @@ -121,9 +170,9 @@ Rollback: `kubectl -n vncmail rollout undo deploy/vncmail-plus` | Symptom | Cause / fix | |---------|-------------| -| Pod `ImagePullBackOff` | `ghcr-pull` secret missing/expired, or package still private. Recreate the secret (§3b) or make the package public. | -| Pod `CrashLoopBackOff`, logs show `EACCES`/permission on `/app/data` | Volume not writable by uid 1001. `securityContext.fsGroup: 1001` is set in `deployment.yaml` — keep it; some storage drivers also need it on the PVC. | -| PVC stuck `Pending` | Wrong `storageClassName` in `pvc.yaml`. Set it to one from `kubectl get sc`. | +| Pod `ImagePullBackOff` | `ghcr-pull` secret missing/expired, or package still private. Recreate the secret (§3a) or make the package public. | +| Pod `CrashLoopBackOff`, logs show `EACCES`/permission on `/app/data` | Volume not writable by uid 1001. `securityContext.fsGroup: 1001` is set in `base/deployment.yaml` — keep it; some storage drivers also need it on the PVC. | +| PVC stuck `Pending` | Wrong `storageClassName` in `base/pvc.yaml`. Set it to one from `kubectl get sc`. | | Ingress has no address / no cert | Wrong `ingressClassName` or cert issuer. Match bulwark's (§2). Check `kubectl -n vncmail describe ingress vncmail-plus`. | | Login shows "Ein Fehler ist aufgetreten" | Use the **full** email (`user@sandbox.vnc.de`), not a bare username. | | Can't reach Stalwart | Check `JMAP_SERVER_URL` in the secret = `https://stalwart.sandbox.vnc.de`. | diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/base/deployment.yaml similarity index 82% rename from deploy/k8s/deployment.yaml rename to deploy/k8s/base/deployment.yaml index c9dfae3e..f72e952c 100644 --- a/deploy/k8s/deployment.yaml +++ b/deploy/k8s/base/deployment.yaml @@ -2,7 +2,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: vncmail-plus - namespace: vncmail labels: app: vncmail-plus spec: @@ -26,12 +25,17 @@ spec: runAsGroup: 1001 # ghcr package is private by default — see deploy/k8s/README.md to create # this pull secret. Delete this block if you make the package public. + # NOTE: once CI moves to pushing registry.gitlab.vnc.biz images (the + # dev-auto-deploy phase of the GitLab pipeline), this needs to become a + # docker-registry secret for that registry instead — comments only, + # deliberately not renamed here, so this file stays a no-op today. imagePullSecrets: - name: ghcr-pull containers: - name: vncmail-plus - # dev image (built from the `dev` branch by CI). For production pin a - # digest: ghcr.io/brvncde-dotcom/vncmail-plus-dev@sha256: + # Default/legacy value — CI overrides the image per-deploy via + # `kustomize edit set image`, so what's committed here never goes + # stale. For a one-off manual apply, pin a digest instead of :latest. image: ghcr.io/brvncde-dotcom/vncmail-plus-dev:latest imagePullPolicy: Always ports: diff --git a/deploy/k8s/base/ingress.yaml b/deploy/k8s/base/ingress.yaml new file mode 100644 index 00000000..24dbb55e --- /dev/null +++ b/deploy/k8s/base/ingress.yaml @@ -0,0 +1,34 @@ +# Both real clusters (node1-3 "prod", dev-k8s-1-3 "dev") run Traefik, not +# nginx — confirmed via `kubectl get ingressclass` (class is literally named +# `traefik`). Unlike nginx's restrictive 1MB default, Traefik has no default +# request-body-size cap, so there's no equivalent needed for mail attachment +# uploads (the old nginx.ingress.kubernetes.io/proxy-body-size annotation +# this file used to carry is simply not applicable here). +# +# Host, TLS secretName, and cert-manager issuer are ALL overlay-specific now +# (dev-k8s only has a `letsencrypt-staging` issuer; node1-3/prod has none +# configured yet) — every overlay's patch-ingress.yaml must override the +# CHANGEME placeholders below. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vncmail-plus + annotations: + cert-manager.io/cluster-issuer: CHANGEME +spec: + ingressClassName: traefik + tls: + - hosts: + - CHANGEME.invalid + secretName: vncmail-plus-tls + rules: + - host: CHANGEME.invalid + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: vncmail-plus + port: + number: 80 diff --git a/deploy/k8s/base/kustomization.yaml b/deploy/k8s/base/kustomization.yaml new file mode 100644 index 00000000..b9a4933e --- /dev/null +++ b/deploy/k8s/base/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - pvc.yaml + - deployment.yaml + - service.yaml + - ingress.yaml + # - secret.yaml # create from an overlay's secret.example.yaml; not committed + +# Namespace is intentionally NOT set here. Kustomize's `namespace:` transformer +# doesn't rename cluster-scoped Namespace objects, so each overlay ships its own +# namespace.yaml (the actual object) and its own `namespace:` field (which +# injects metadata.namespace into every namespaced resource below). Applying +# this base directly is meaningless — always go through an overlay. diff --git a/deploy/k8s/pvc.yaml b/deploy/k8s/base/pvc.yaml similarity index 92% rename from deploy/k8s/pvc.yaml rename to deploy/k8s/base/pvc.yaml index 7d93a4f3..e9dd344e 100644 --- a/deploy/k8s/pvc.yaml +++ b/deploy/k8s/base/pvc.yaml @@ -5,7 +5,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: vncmail-settings - namespace: vncmail spec: accessModes: [ReadWriteOnce] storageClassName: microk8s-hostpath @@ -17,7 +16,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: vncmail-admin - namespace: vncmail spec: accessModes: [ReadWriteOnce] storageClassName: microk8s-hostpath @@ -29,7 +27,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: vncmail-admin-state - namespace: vncmail spec: accessModes: [ReadWriteOnce] storageClassName: microk8s-hostpath @@ -41,7 +38,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: vncmail-telemetry - namespace: vncmail spec: accessModes: [ReadWriteOnce] storageClassName: microk8s-hostpath diff --git a/deploy/k8s/service.yaml b/deploy/k8s/base/service.yaml similarity index 90% rename from deploy/k8s/service.yaml rename to deploy/k8s/base/service.yaml index 1dd0488a..63f90db4 100644 --- a/deploy/k8s/service.yaml +++ b/deploy/k8s/base/service.yaml @@ -2,7 +2,6 @@ apiVersion: v1 kind: Service metadata: name: vncmail-plus - namespace: vncmail labels: app: vncmail-plus spec: diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/ingress.yaml deleted file mode 100644 index cd2392a6..00000000 --- a/deploy/k8s/ingress.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Exposes VNCmail+ at vncmail.sandbox.vnc.de, alongside bulwark.sandbox.vnc.de. -# MATCH YOUR CLUSTER — inspect the existing Bulwark ingress and copy its -# ingressClassName + TLS/cert-manager annotations: -# kubectl get ingress -A | grep bulwark -# kubectl get ingress -n -o yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: vncmail-plus - namespace: vncmail - annotations: - # cert-manager issuer — set to whatever bulwark.sandbox.vnc.de uses. - cert-manager.io/cluster-issuer: letsencrypt-prod - # Mail attachments can be large; raise the nginx body limit. - nginx.ingress.kubernetes.io/proxy-body-size: "100m" -spec: - # microk8s ingress addon class is usually "public" (nginx). Confirm with - # `kubectl get ingressclass` and match bulwark's. - ingressClassName: public - tls: - - hosts: - - vncmail.sandbox.vnc.de - secretName: vncmail-plus-tls - rules: - - host: vncmail.sandbox.vnc.de - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: vncmail-plus - port: - number: 80 diff --git a/deploy/k8s/overlays/dev/image-tag/kustomization.yaml b/deploy/k8s/overlays/dev/image-tag/kustomization.yaml new file mode 100644 index 00000000..25cc7058 --- /dev/null +++ b/deploy/k8s/overlays/dev/image-tag/kustomization.yaml @@ -0,0 +1,11 @@ +# Owned by CI (the bump-dev job in .gitlab-ci.yml), not by hand. Kept as its +# own Component so CI only ever rewrites this 6-line file, never the parent +# overlays/dev/kustomization.yaml (structure/patches there stay under normal +# code review — CI regenerating a whole hand-maintained file on every push +# would silently revert any change made there between deploys). +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +images: + - name: ghcr.io/brvncde-dotcom/vncmail-plus-dev + newName: ghcr.io/brvncde-dotcom/vncmail-plus-dev + newTag: latest diff --git a/deploy/k8s/overlays/dev/kustomization.yaml b/deploy/k8s/overlays/dev/kustomization.yaml new file mode 100644 index 00000000..95540993 --- /dev/null +++ b/deploy/k8s/overlays/dev/kustomization.yaml @@ -0,0 +1,18 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: vncmail +resources: + - ../../base + - namespace.yaml + # - secret.yaml # create from secret.example.yaml; not committed + +patches: + - path: patch-ingress.yaml + +components: + - image-tag + +# Targets the dev-k8s-1/2/3 cluster (confirmed via direct access: this is +# where ArgoCD already lives). The image tag lives in image-tag/ (a separate +# Component CI owns — see .gitlab-ci.yml's bump-dev job) rather than here, so +# CI never needs to touch this file. diff --git a/deploy/k8s/namespace.yaml b/deploy/k8s/overlays/dev/namespace.yaml similarity index 100% rename from deploy/k8s/namespace.yaml rename to deploy/k8s/overlays/dev/namespace.yaml diff --git a/deploy/k8s/overlays/dev/patch-ingress.yaml b/deploy/k8s/overlays/dev/patch-ingress.yaml new file mode 100644 index 00000000..f9efd41a --- /dev/null +++ b/deploy/k8s/overlays/dev/patch-ingress.yaml @@ -0,0 +1,27 @@ +# dev-k8s cluster confirmed to have a `letsencrypt-staging` ClusterIssuer +# already (no `letsencrypt-prod` exists there) - staging avoids burning +# Let's Encrypt's real rate limits while this is still being stood up. +# vncmail.sandbox.vnc.de DNS does not point here yet either - this is the +# intended host, not a live one (see VNCMAIL-SETUP.md for what's still open). +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vncmail-plus + annotations: + cert-manager.io/cluster-issuer: letsencrypt-staging +spec: + tls: + - hosts: + - vncmail.sandbox.vnc.de + secretName: vncmail-plus-tls + rules: + - host: vncmail.sandbox.vnc.de + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: vncmail-plus + port: + number: 80 diff --git a/deploy/k8s/secret.example.yaml b/deploy/k8s/overlays/dev/secret.example.yaml similarity index 100% rename from deploy/k8s/secret.example.yaml rename to deploy/k8s/overlays/dev/secret.example.yaml diff --git a/deploy/k8s/overlays/prod/image-tag/kustomization.yaml b/deploy/k8s/overlays/prod/image-tag/kustomization.yaml new file mode 100644 index 00000000..7619b621 --- /dev/null +++ b/deploy/k8s/overlays/prod/image-tag/kustomization.yaml @@ -0,0 +1,12 @@ +# Owned by CI (the bump-prod job in .gitlab-ci.yml), not by hand — same +# reasoning as overlays/dev/image-tag/. Starts pointed at an obviously-fake +# tag on purpose: nothing has been promoted yet, and vncmail-prod's ArgoCD +# Application has manual sync anyway, so this being "wrong" doesn't deploy +# anything wrong — it just means there's nothing to sync until a real +# `git push` to main updates it. +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +images: + - name: ghcr.io/brvncde-dotcom/vncmail-plus-dev + newName: registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus + newTag: not-yet-promoted diff --git a/deploy/k8s/overlays/prod/kustomization.yaml b/deploy/k8s/overlays/prod/kustomization.yaml new file mode 100644 index 00000000..0e7d1597 --- /dev/null +++ b/deploy/k8s/overlays/prod/kustomization.yaml @@ -0,0 +1,21 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: vncmail-prod +resources: + - ../../base + - namespace.yaml + # - secret.yaml # create from secret.example.yaml; not committed + +patches: + - path: patch-ingress.yaml + - path: patch-deployment.yaml + +components: + - image-tag + +# NOT MEANT TO BE SYNCED AS COMMITTED. Scaffolding only (see the pipeline +# plan's Phase C/D) — image-tag/'s placeholder tag is obviously-invalid on +# purpose. The bump-prod job in .gitlab-ci.yml keeps that tag pointed at +# whatever's already on dev once main advances, but vncmail-prod's ArgoCD +# Application has manual sync — a human still has to click Sync (or +# `argocd app sync vncmail-prod`) for any of this to actually apply. diff --git a/deploy/k8s/overlays/prod/namespace.yaml b/deploy/k8s/overlays/prod/namespace.yaml new file mode 100644 index 00000000..85f6ab2d --- /dev/null +++ b/deploy/k8s/overlays/prod/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: vncmail-prod + labels: + app.kubernetes.io/part-of: vnclagoon-suite diff --git a/deploy/k8s/overlays/prod/patch-deployment.yaml b/deploy/k8s/overlays/prod/patch-deployment.yaml new file mode 100644 index 00000000..2250714e --- /dev/null +++ b/deploy/k8s/overlays/prod/patch-deployment.yaml @@ -0,0 +1,9 @@ +# Basic HA. Still `strategy: Recreate` (inherited from base) since the PVCs +# are RWO — 2 replicas doesn't buy zero-downtime rollouts by itself, only +# tolerance for a node loss between deploys. Revisit if that's not enough. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vncmail-plus +spec: + replicas: 2 diff --git a/deploy/k8s/overlays/prod/patch-ingress.yaml b/deploy/k8s/overlays/prod/patch-ingress.yaml new file mode 100644 index 00000000..5aef4069 --- /dev/null +++ b/deploy/k8s/overlays/prod/patch-ingress.yaml @@ -0,0 +1,31 @@ +# PLACEHOLDER — the real production hostname has not been decided yet (see +# VNCMAIL-SETUP.md / the pipeline plan). vncmail.CHANGEME.invalid is +# deliberately unresolvable: applying this overlay as committed will not +# issue a cert or route traffic anywhere. Replace both occurrences below, +# and the matching TLS secretName, before Phase D (first real prod deploy). +# +# Targets node1-3 (the HA "prod" cluster). Deliberately does NOT override +# base's `cert-manager.io/cluster-issuer: CHANGEME` — node1-3 has ZERO +# ClusterIssuers configured today (confirmed via direct access). A human +# needs to create a real one there (ACME account, DNS-01 or HTTP-01 solver) +# before this can be anything but a placeholder. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vncmail-plus +spec: + tls: + - hosts: + - vncmail.CHANGEME.invalid + secretName: vncmail-plus-prod-tls + rules: + - host: vncmail.CHANGEME.invalid + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: vncmail-plus + port: + number: 80 diff --git a/deploy/k8s/overlays/prod/secret.example.yaml b/deploy/k8s/overlays/prod/secret.example.yaml new file mode 100644 index 00000000..e6a9fd4c --- /dev/null +++ b/deploy/k8s/overlays/prod/secret.example.yaml @@ -0,0 +1,28 @@ +# Copy to secret.yaml, fill in real values, and apply. DO NOT commit secret.yaml +# (it is gitignored). Generate SESSION_SECRET with: openssl rand -base64 32 +# +# JMAP_SERVER_URL is a PLACEHOLDER — there is no production Stalwart instance +# yet. This overlay cannot go live (Phase D) until one exists and this value +# points at it for real. +apiVersion: v1 +kind: Secret +metadata: + name: vncmail-env + namespace: vncmail-prod +type: Opaque +stringData: + # Core — connect to Stalwart over JMAP + JMAP_SERVER_URL: "https://REPLACE-ME-prod-stalwart-not-yet-deployed.invalid" + SESSION_SECRET: "REPLACE_ME__openssl_rand_base64_32" + # Branding (theme defaults to VNClagoon in code; these set name + logo) + APP_NAME: "VNCmail+" + APP_SHORT_NAME: "VNCmail+" + LOGIN_COMPANY_NAME: "VNClagoon" + LOGIN_LOGO_DARK_URL: "/branding/vncmail-wordmark-on-dark.svg" + LOGIN_LOGO_LIGHT_URL: "/branding/vncmail-wordmark-on-light.svg" + APP_LOGO_DARK_URL: "/branding/vncmail-wordmark-on-dark.svg" + APP_LOGO_LIGHT_URL: "/branding/vncmail-wordmark-on-light.svg" + LOGIN_LOGO_MAX_HEIGHT: "52" + # Housekeeping + BULWARK_UPDATE_CHECK: "off" + # Data dirs default to /app/data/* (mounted to the PVCs) — no need to set them. diff --git a/lib/smime-ca/ejbca.ts b/lib/smime-ca/ejbca.ts index 61dd4028..dccddc0f 100644 --- a/lib/smime-ca/ejbca.ts +++ b/lib/smime-ca/ejbca.ts @@ -210,7 +210,9 @@ function escapeDn(value: string): string { .replace(/([\\,+"<>;=])/g, '\\$1') .replace(/^([ #])/, '\\$1') .replace(/ $/, '\\ ') - // Control characters have no legitimate place in a DN. + // Control characters have no legitimate place in a DN — the class below + // is intentional, not a typo. + // eslint-disable-next-line no-control-regex .replace(/[\x00-\x1f\x7f]/g, ''); }