docs: update CLAUDE.md and README.md to include me.nik4nao.com deployment details
Some checks failed
validate / lint (push) Failing after 1s

This commit is contained in:
Nik Afiq 2026-09-16 23:30:38 +09:00
parent 471e9ae8ca
commit 2c26c227ee
3 changed files with 66 additions and 2 deletions

View File

@ -140,3 +140,8 @@ otherwise it reports spurious `unknown-module` errors for `community.general`/
`manifests/network/pihole-debian-patch.sh`. `manifests/network/pihole-debian-patch.sh`.
- Dashy's manifest ships an empty ConfigMap shell; real config only lands - Dashy's manifest ships an empty ConfigMap shell; real config only lands
after running `manifests/core/apply-dashy-config.sh`. after running `manifests/core/apply-dashy-config.sh`.
- A newly pushed Argo CD `Application` doesn't create its namespace
instantly — `app-of-apps` only picks it up on its next Git poll. A
namespace-scoped secret script run immediately after pushing can fail
with `namespaces "<ns>" not found` until then (or until a forced
refresh).

View File

@ -40,7 +40,7 @@ Public services under `nik4nao.com` use Let's Encrypt.
| Git and CI | Gitea, Gitea Actions runner, registry pull secrets, Gitea backup CronJob | | Git and CI | Gitea, Gitea Actions runner, registry pull secrets, Gitea backup CronJob |
| Media | Jellyfin, qBittorrent, JDownloader | | Media | Jellyfin, qBittorrent, JDownloader |
| Home services | Home Assistant ingress, HA gateway, AI gateway, Discord bot, Alexa bridge, Alert bridge | | Home services | Home Assistant ingress, HA gateway, AI gateway, Discord bot, Alexa bridge, Alert bridge |
| Public apps | Portfolio, Watch Party ingress to the Mac Mini | | Public apps | Portfolio, me.nik4nao.com, Watch Party ingress to the Mac Mini |
| Dashboard | Dashy | | Dashboard | Dashy |
## Important URLs ## Important URLs
@ -64,6 +64,7 @@ Public services under `nik4nao.com` use Let's Encrypt.
| `http://alert-bridge.home.arpa` | Alert bridge (Discord webhook relay) | Plain HTTP | | `http://alert-bridge.home.arpa` | Alert bridge (Discord webhook relay) | Plain HTTP |
| `https://watch-party.nik4nao.com` | Watch Party on Mac Mini | Let's Encrypt | | `https://watch-party.nik4nao.com` | Watch Party on Mac Mini | Let's Encrypt |
| `https://nik4nao.com` | Portfolio | Let's Encrypt | | `https://nik4nao.com` | Portfolio | Let's Encrypt |
| `https://me.nik4nao.com` | me.nik4nao.com (terminal-style personal site) | Let's Encrypt |
| `http://ca.home.arpa` | Internal CA installer | Plain HTTP | | `http://ca.home.arpa` | Internal CA installer | Plain HTTP |
`home.arpa` names are defined explicitly in `values/pihole.yaml` and `home.arpa` names are defined explicitly in `values/pihole.yaml` and
@ -190,6 +191,7 @@ bash manifests/home-services/registry-secret.sh
bash manifests/monitoring/grafana-secret.sh bash manifests/monitoring/grafana-secret.sh
bash manifests/network/ddns-secret.sh bash manifests/network/ddns-secret.sh
bash manifests/portfolio/registry-secret.sh bash manifests/portfolio/registry-secret.sh
bash manifests/me-nik4nao-com/registry-secret.sh
``` ```
Sealed Secret regeneration: Sealed Secret regeneration:
@ -223,7 +225,8 @@ The Debian NFS server exports both `/mnt/storage` and `/home/nik/backups` to
## TLS and Trust ## TLS and Trust
`*.home.arpa` certificates use `internal-ca-issuer` from `manifests/cert-manager`. `*.home.arpa` certificates use `internal-ca-issuer` from `manifests/cert-manager`,
which signs off the self-signed root `internal-ca-cert` (`cert-manager` namespace).
The CA installer at `http://ca.home.arpa` serves `ca.crt` and an iOS/macOS The CA installer at `http://ca.home.arpa` serves `ca.crt` and an iOS/macOS
mobileconfig profile. The `ca-sync` CronJob updates those files from the mobileconfig profile. The `ca-sync` CronJob updates those files from the
`cert-manager/internal-ca-cert` secret when the CA changes. `cert-manager/internal-ca-cert` secret when the CA changes.
@ -231,6 +234,49 @@ mobileconfig profile. The `ca-sync` CronJob updates those files from the
`*.nik4nao.com` certificates use the Let's Encrypt issuers in `*.nik4nao.com` certificates use the Let's Encrypt issuers in
`manifests/cert-manager/cluster-issuer-letsencrypt.yaml`. `manifests/cert-manager/cluster-issuer-letsencrypt.yaml`.
### Renewing the internal CA
`internal-ca-cert`'s default `privateKey.rotationPolicy` is `Never`, so a
normal renewal reuses the existing private key — every already-issued leaf
cert and every device that already trusts the CA keep working unchanged.
**Deleting the `internal-ca-cert` secret instead of renewing it in place
generates a brand-new key pair**, which invalidates every leaf cert and every
device's trust simultaneously (this happened 2026-09-02: leaf certs issued
before the deletion failed `openssl verify` against the recreated CA
entirely, and Jellyfin/Kavita/etc. all showed as insecure on client devices).
To renew correctly:
```bash
cmctl renew certificate internal-ca-cert -n cert-manager
```
This reissues the CA certificate (new serial/validity) while keeping the same
key, so it should not require any downstream action. If the key ever *does*
change (intentional rotation, or the secret was lost and had to be
recreated), do both of the following — the CA rotating does **not**
automatically cascade to either:
1. **Force-reissue every leaf cert**, since each `Certificate` renews on its
own independent schedule and won't notice the issuing CA changed:
```bash
for c in $(kubectl get certificate -A -o json | \
jq -r '.items[] | select(.spec.issuerRef.name=="internal-ca-issuer") | "\(.metadata.namespace)/\(.metadata.name)"'); do
ns="${c%%/*}"; name="${c##*/}"
secret=$(kubectl get certificate "$name" -n "$ns" -o jsonpath='{.spec.secretName}')
kubectl delete secret "$secret" -n "$ns"
done
```
Verify afterward that every leaf chains to the current CA:
```bash
kubectl get secret internal-ca-cert -n cert-manager -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/current-ca.crt
openssl verify -CAfile /tmp/current-ca.crt <(kubectl get secret <leaf-secret> -n <ns> -o jsonpath='{.data.tls\.crt}' | base64 -d)
```
2. **Re-trust the CA on every client device** — visit `http://ca.home.arpa`,
reinstall the mobileconfig, and re-enable full trust under Settings →
General → About → Certificate Trust Settings (iOS/macOS). The old trust
profile does not update itself; nothing pushes the new CA to devices.
## VPN VLAN 50 (PIA gateway) — status and runbook ## VPN VLAN 50 (PIA gateway) — status and runbook
`minisforum` is a PIA WireGuard egress gateway for a dedicated VPN VLAN `minisforum` is a PIA WireGuard egress gateway for a dedicated VPN VLAN
@ -329,8 +375,20 @@ plane.
- Authentik forward-auth depends on the `Cookie` header in - Authentik forward-auth depends on the `Cookie` header in
`authRequestHeaders`; removing it causes redirect loops. `authRequestHeaders`; removing it causes redirect loops.
- CoreDNS custom zone snippets must use the `.server` key suffix. - CoreDNS custom zone snippets must use the `.server` key suffix.
- Renewing `internal-ca-cert` by deleting its secret (instead of `cmctl
renew`) generates a new key pair, silently breaking every leaf cert and
every device's trust at once — see "Renewing the internal CA" under [TLS
and Trust](#tls-and-trust) for the recovery/prevention procedure.
- The Dashy manifest contains an empty ConfigMap shell; use - The Dashy manifest contains an empty ConfigMap shell; use
`manifests/core/apply-dashy-config.sh` to load the real config. `manifests/core/apply-dashy-config.sh` to load the real config.
- A newly pushed Argo CD `Application` (a new file under `argocd/apps/`)
doesn't create its namespace/resources the instant you push — `app-of-apps`
only picks it up on its next Git poll. Running a namespace-scoped
`registry-secret.sh` (or anything else assuming the namespace already
exists) right after pushing can fail with `namespaces "<ns>" not found`;
either wait for the next poll or force one: `kubectl patch application
app-of-apps -n argocd --type merge -p
'{"metadata":{"annotations":{"argocd.argoproj.io/refresh":"hard"}}}'`.
See the scoped READMEs in `ansible/`, `argocd/`, and `manifests/` for workflows See the scoped READMEs in `ansible/`, `argocd/`, and `manifests/` for workflows
specific to those directories. specific to those directories.

View File

@ -14,6 +14,7 @@ subdirectories are consumed by Argo CD Applications in `argocd/apps`.
| `gitea/` | Gitea storage, backup, public ingress, runner and OIDC/admin secrets | | `gitea/` | Gitea storage, backup, public ingress, runner and OIDC/admin secrets |
| `home-services/` | HA gateway, AI gateway, Discord bot, service TLS, registry secret | | `home-services/` | HA gateway, AI gateway, Discord bot, service TLS, registry secret |
| `homeassistant/` | Home Assistant external service and ingress | | `homeassistant/` | Home Assistant external service and ingress |
| `me-nik4nao-com/` | me.nik4nao.com deployment, ingress, registry pull secret |
| `media/` | Jellyfin, qBittorrent, JDownloader ([sending links](media/jdownloader.md)) | | `media/` | Jellyfin, qBittorrent, JDownloader ([sending links](media/jdownloader.md)) |
| `monitoring/` | Monitoring PVs, Grafana datasource, Grafana/Auth OIDC secrets | | `monitoring/` | Monitoring PVs, Grafana datasource, Grafana/Auth OIDC secrets |
| `multus/` | Multus CNI, VLAN 50 NetworkAttachmentDefinition, canary, browser-vpn-proxy SOCKS5 proxy ([details](multus/README.md)) — manual-sync Application, see `argocd/apps/multus.yaml` | | `multus/` | Multus CNI, VLAN 50 NetworkAttachmentDefinition, canary, browser-vpn-proxy SOCKS5 proxy ([details](multus/README.md)) — manual-sync Application, see `argocd/apps/multus.yaml` |