From 2ffbfc4034c59420933778df78f0a142f3b7514b Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 1 Jun 2026 23:30:55 +0200 Subject: [PATCH] docs(infra): document team mkcert CA on vm-gitlab (cross-VM trust) (#264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Doc-only follow-up to the just-shipped ADR-0030 dockerised dev mode + dev-server TLS (PR #263). Adds a "Team mkcert CA on `vm-gitlab`" subsection to `infra/README.md` so a teammate joining the project can browse any dev VM with a green padlock (cross-VM access) without each pair of devs having to swap their private CAs. Hits a real need: a third developer is about to onboard, and the current single-dev mkcert procedure doesn't scale beyond one — each dev's solo CA is only trusted by their own workstation, so colleagues hitting another VM see a `NET::ERR_CERT_AUTHORITY_INVALID` warning every time. ## What lands `infra/README.md` — one new subsection added immediately after the existing "HTTPS dev-server setup" block, under the same Local-dev-stack section. No other file touched. The subsection documents: 1. **Initial setup on `vm-gitlab`** — install mkcert, create a root-only `CAROOT` at `/srv/apf-portal/mkcert-ca/`, generate the team CA there. Run once by the R&D Lead. 2. **Minting per-VM certs** — the canonical `mkcert -key-file … -cert-file … apf-portal..local` invocation pointed at the shared `CAROOT`, plus a sanity `openssl x509 -subject -issuer` step and the `scp` to the target VM's `~/Works/apf_portal/.secrets/dev-tls.{key,pem}`. 3. **Onboarding a new developer** — what flows where: - R&D Lead → new dev: `rootCA.pem` (public cert, secure channel — 1Password / Bitwarden / direct scp, never plain e-mail) + the per-VM cert pair already on their VM. - New dev's workstation: drop `rootCA.pem` into the local `mkcert -CAROOT`, run `mkcert -install` to push the team CA into the Windows trust store. - Standard `hosts` / `.env` / `NX_SERVE_CONFIGURATION=https` / `dev.sh up apps` follow. 4. **Operational notes** — departures (no CRL needed because the dev never held the private key), CA rotation, per-VM cert rotation, future migration to a corp-CA-signed cert. ## Why `vm-gitlab` as the CA host The CA itself is just two files (`rootCA.pem` + `rootCA-key.pem`); it does not need a service running. `vm-gitlab` is the natural home for a few reasons: - It is already a **shared, team-managed infra VM**, owned by the same person who would mint the certs anyway. - The CA outlives any individual dev's laptop or workstation reinstall. - Restricting the directory to `root` keeps the private key out of every developer's blast radius — only the R&D Lead with `sudo` on `vm-gitlab` can mint. The R&D Lead becomes the steward; developers never need SSH access to `vm-gitlab`. That trade — slight bottleneck at onboarding for much smaller key-exposure surface — is the right balance at small team scale. ## Why not just distribute the CA key Considered the alternative — every dev gets both `rootCA.pem` and `rootCA-key.pem` in their local mkcert `CAROOT` so they can mint their own certs. Pros: no bottleneck. Cons: the CA private key would live on N workstations, and anyone with it can forge a trusted cert for any hostname on any teammate's machine. Acceptable at 2 devs of complete trust; risky at 3+. The steward pattern scales without that trade. ## Test plan - [x] Doc renders cleanly in the `infra/README.md` flow (subsection lands between "HTTPS dev-server setup" and "Service endpoints (defaults)"). - [ ] R&D Lead walks the "Initial setup on `vm-gitlab`" steps and confirms the CA files end up at `/srv/apf-portal/mkcert-ca/` with the documented permissions. - [ ] First new-dev onboarding (the upcoming third dev) follows the section end-to-end and reaches `https://apf-portal.dev-.local:4200/` with a green padlock. - [ ] Verify the "browse from one dev's workstation to another dev's VM" promise: after the team CA is installed on at least two workstations, both can browse `https://apf-portal.dev-jg.local:4200/` and `https://apf-portal.dev-vc.local:4200/` without a cert warning. ## Related - [ADR-0030](docs/decisions/0030-dockerised-dev-mode.md) — the dockerised dev mode this completes for team-scale operation. - [ADR-0028](docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) — places `vm-gitlab` as shared infra; this PR uses it as the natural CA host. - PR #263 (`feat(spa): opt-in 'https' nx serve config for dev-server TLS`) — provides the dev-server TLS plumbing this section instructs how to feed. --------- Co-authored-by: Julien Gautier Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/264 --- infra/README.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/infra/README.md b/infra/README.md index 1f9285d..2f31031 100644 --- a/infra/README.md +++ b/infra/README.md @@ -270,6 +270,117 @@ Native `nx serve` (WSL / localhost) is **unaffected** — it keeps using the `de When real DNS + corp-CA-signed certs arrive, the hostname can be reused as-is (Entra registrations are literal strings — they don't care who signs the cert). Drop the cert files back into `.secrets/` and remove the mkcert step. +### Team mkcert CA on `vm-gitlab` — sharing the trust root + +The previous section is the **solo flow** (one dev mints their own CA, certs only trusted by their own workstation). It does not let a teammate browse another dev's VM without a certificate warning — every dev has their own private CA, none of which the others trust. + +For a multi-dev team the canonical pattern is one shared CA held on `vm-gitlab`. The CA private key (`rootCA-key.pem`) stays on `vm-gitlab` — never copied to any workstation; only the public `rootCA.pem` is distributed to each developer's Windows trust store, and the R&D Lead mints per-VM certs on `vm-gitlab` when a new VM (or new developer) joins. Browsing any dev VM from any workstation then "just works" — green padlock, no warning. + +This subsection assumes the per-dev workstation procedure of "HTTPS dev-server setup" above is what every developer will do **once**, with the rootCA.pem they receive from this shared CA. + +#### Initial setup on `vm-gitlab` (one-time, by the R&D Lead) + +```bash +# 1. Install mkcert on vm-gitlab (no service to run — mkcert is one-shot). +sudo curl -fsSL https://dl.filippo.io/mkcert/latest?for=linux/amd64 \ + -o /usr/local/bin/mkcert +sudo chmod +x /usr/local/bin/mkcert + +# 2. Create the shared CAROOT, root-only. +sudo mkdir -p /srv/apf-portal/mkcert-ca +sudo chown root:root /srv/apf-portal/mkcert-ca +sudo chmod 700 /srv/apf-portal/mkcert-ca + +# 3. Generate the CA into that CAROOT. (`-install` here just touches +# the local trust store of vm-gitlab — cosmetic for an infra VM, +# no harm.) +sudo CAROOT=/srv/apf-portal/mkcert-ca mkcert -install + +# 4. Verify. +sudo ls -la /srv/apf-portal/mkcert-ca/ +# → rootCA.pem (-rw-r--r--), rootCA-key.pem (-rw-------, root only) +``` + +After this, the CA exists and is owned by `root` on `vm-gitlab`. Developers never touch it directly. + +#### Minting a cert for a dev VM (R&D Lead, on `vm-gitlab`) + +Repeat once per VM hostname (`apf-portal.dev-jg.local`, `apf-portal.dev-vc.local`, `apf-portal.dev.local`, …). Replace `` and the SSH/scp target accordingly: + +```bash +sudo CAROOT=/srv/apf-portal/mkcert-ca mkcert \ + -key-file /tmp/-tls.key \ + -cert-file /tmp/-tls.pem \ + apf-portal..local + +# Sanity check. +sudo openssl x509 -in /tmp/-tls.pem -noout -subject -issuer +# subject CN must be apf-portal..local; issuer the mkcert CA name. + +# Ship to the target VM, renaming to the path the `https` Nx serve +# configuration expects (.secrets/dev-tls.{key,pem}). +sudo scp /tmp/-tls.key :~/Works/apf_portal/.secrets/dev-tls.key +sudo scp /tmp/-tls.pem :~/Works/apf_portal/.secrets/dev-tls.pem + +# Wipe the staging copies. +sudo rm /tmp/-tls.* +``` + +The certificate is good for ~2 years (mkcert default). When it nears expiry, regenerate with the same command and re-`scp` — the dev-server picks up the new files on next restart. + +#### Onboarding a new developer + +A new teammate needs **three things**: a copy of `rootCA.pem` (public, low-sensitivity), a per-VM cert minted by the R&D Lead, and the same hosts-file + `.env` configuration every dev follows. + +**R&D Lead side** — on `vm-gitlab`: + +```bash +# Hand off the public CA cert to the new dev via a secure channel +# (1Password shared vault, Bitwarden, direct scp). Never plain e-mail. +sudo cat /srv/apf-portal/mkcert-ca/rootCA.pem +``` + +Then mint that dev's per-VM cert (see "Minting a cert for a dev VM" above) and ship it to their VM's `~/Works/apf_portal/.secrets/`. + +**New developer side** — on their Windows workstation: + +```powershell +# 1. Install mkcert (only to get the `-install` command — no need to +# generate certs on the workstation). +choco install mkcert -y + +# 2. Drop the rootCA.pem they received into the local CAROOT path. +$caroot = mkcert -CAROOT +Copy-Item "C:\path\to\rootCA.pem" "$caroot\rootCA.pem" +# NB: only rootCA.pem — they do NOT receive rootCA-key.pem. + +# 3. Register the team CA in their Windows trust store. +mkcert -install +# Confirm the Windows security dialog. Their machine now trusts every +# cert minted by the team CA on vm-gitlab. +``` + +Then they: + +- Edit `C:\Windows\System32\drivers\etc\hosts` (admin) and add the entries for every VM they want to reach (their own + the others as needed): + ``` + 10.100.201.20 apf-portal.dev-vc.local + 10.100.201.21 apf-portal.dev-jg.local + 10.100.201.22 apf-portal.dev.local + ``` +- Edit `apps/portal-bff/.env` on their VM so the four `ENTRA_*_REDIRECT_URI` values point at `https://apf-portal.:{4200,4300}/...` (the matching URIs are already registered Entra-side — no action there). +- Set `NX_SERVE_CONFIGURATION=https` in `infra/local/.env` on their VM. +- `./infra/local/dev.sh down && ./infra/local/dev.sh up apps`. + +Total onboarding budget: ~5 min of R&D Lead time on `vm-gitlab` (mint + transfer) + ~10 min of work on the new dev's workstation + VM. No SSH access to `vm-gitlab` is granted to developers — only the R&D Lead operates the CA. + +#### Operational notes + +- **Departures.** mkcert has no CRL; revoking trust on a former dev's machine isn't actionable from the CA side. The risk surface is what that dev could have signed before leaving — and they only ever had the public `rootCA.pem`, never the private key, so they cannot have signed anything in your trust circle. No action required when a dev leaves. +- **CA rotation.** Rare (audit, suspected compromise, annual hygiene). Regenerate the CA on `vm-gitlab`, re-mint every VM's cert, redistribute the new `rootCA.pem` to each dev. Each dev re-imports + re-`mkcert -install`. No `.env` or Entra change. +- **Per-VM cert rotation.** Same pattern as initial mint — regenerate, scp, `dev.sh restart portal-shell portal-admin`. No client-side action. +- **Migration to a corp-signed CA.** When the infra team issues an internal-CA-signed cert (already trusted by every domain-joined workstation, no mkcert step), drop those files into `.secrets/dev-tls.{key,pem}` and remove the team mkcert CA from each dev's trust store. Entra registrations are unchanged — they reference hostname + port, not the issuer. + ### Service endpoints (defaults) | Service | Host port | Purpose |