What you must change¶
This is the customization surface — every place alpstack is wired to our cluster, domain and cloud, that a different operator has to redirect. It's the answer to "if I wanted to deploy this, what would I need to change?".
How to read this
Couplings fall into three kinds:
- :material-cog: Global — driven by
values/global.yaml; set it once. The entire domain / realm / CA surface is global-driven — including literals Helm can't template in a subchartvalues.yaml, which are injected via the app-of-apps Application's inlinehelm.values. - :material-map-marker: Site-specific — a physical or cloud fact (an IP, a NodePort, an S3 endpoint) with no shared default. You replace it with your fact.
- :material-link-variant: Entwined (upstream render) — lives inside the
openDesk helmfile render (DB names, realm, Matrix
server_name). Change these as a set via the render inputs, never by hand.
The tables below map where each coupling lives. Most are driven from
values/global.yaml; the ones that need a manual edit — only
genuinely per-site facts — are collected under
Configuration values.
The cross-cutting couplings¶
Two couplings appear in every layer:
| Coupling | Ours | Appears | Kind |
|---|---|---|---|
| Base domain | coeur.li |
~160 literals across all four repos — ingress hosts, cert SANs, OIDC issuer URLs, mail, Matrix federation, TURN realm | :material-cog: global.dns.baseDomain |
| Internal domain | apps.int.coeur.li |
every internal HTTPRoute host, Dex issuer, OIDC redirect URIs, cookieDomain |
:material-cog: global.dns.appsDomain |
| Cluster name | coeurli |
cert-manager coeurli-ca/coeurli-issuer, the clusters/coeurli/ overlay dir, 5 VMRule names, NetBird network name, cluster-digest subjects, each <name>-argocd UI domain |
:material-cog: global.cluster.name (+ derived caName/issuerName) |
| Cluster CA | the coeurli-ca root cert |
ArgoCD's Dex OIDC oidc.config rootCA + headlamp's CA mount — one copy for every consumer |
:material-cog: global.cluster.rootCA |
DNS spans nearly every chart and HTTPRoute. cluster.name drives the cert-manager CA and
ClusterIssuer (the TLS trust chain), the VMRule names, and the clusters/coeurli/ overlay
directory. The ops-proxy hostnames, the Dex issuer + redirect URIs, and oauth2-proxy's
OIDC args are all global-driven (injected via the app-of-apps helm.values). One
change isn't a value: rename the
clusters/coeurli/ directory to clusters/<your-name>/ and update the app-of-apps
path: accordingly.
Layer 1 — Talos¶
Repo: talos-cluster. Nearly everything site-specific here is a
talosctl gen config input or a literal inside a patch-*.yaml. The patch bodies
(CNI/proxy-off, disk encryption, kubelet-csr-approver, API-server tuning) are
generic — only their embedded IPs/domains are yours.
| What | Ours | Where | Replace with |
|---|---|---|---|
| Cluster name | coeurli |
talosctl gen config (README) |
:material-cog: your cluster name |
| API endpoint + SANs | https://172.16.15.1:6443, api.int.coeur.li |
gen-config args + --additional-sans |
:material-map-marker: your VIP/endpoint |
| Install disk | /dev/vda |
gen-config | :material-map-marker: your disk |
| Control-plane IPs | 172.16.15.190-192 |
apply-config, patch-controlplane-metrics-cp0{1,2,3}.yaml (one per CP) |
:material-map-marker: your CP IPs |
| Node subnet | 172.16.15.0/24 |
patch-etcd.yaml, patch-kubelet.yaml, patch-kubelet-csr-approver.yaml (PROVIDER_REGEX) |
:material-map-marker: your subnet |
| Nameserver / NTP | 172.16.15.1 |
patch-network.yaml, patch-timeserver.yaml |
:material-map-marker: your gateway |
| Dex OIDC issuer | https://dex.apps.int.coeur.li |
patch-api-server.yaml |
:material-cog: your apps.int domain |
| Embedded CA cert | coeurli-ca.crt (inline) + /var/coeurli-ca.crt path |
patch-api-server.yaml |
:material-map-marker: your cluster CA |
| Image-factory schematic IDs | CP/worker schematic hashes | README | regenerate for your extension set |
Reusable as-is
patch-no-cni-and-proxy.yaml and patch-disk-encryption.yaml need no edits.
Cilium replaces kube-proxy, so there is no VIP and no custom pod/service CIDR —
Talos defaults are used.
Layer 2 — Platform¶
Repo: platform-argocd. Most of the DNS and cluster-name couplings live here.
| What | Ours | Where (representative) | Replace with |
|---|---|---|---|
| Internal wildcard | *.apps.int.coeur.li |
internal-gateway/values.yaml, every */templates/httproute.yaml |
:material-cog: global.dns.appsDomain |
| Dex redirect URIs | 8 static https://<svc>.apps.int.coeur.li/... |
dex/values.yaml |
:material-cog: derive per-service from appsDomain |
| cert-manager CA/issuer | coeurli-ca, coeurli-issuer |
cert-manager/templates/{ca,issuer}.yaml, internal-gateway/values.yaml, headlamp CA mount |
:material-cog: global.cluster.{caName,issuerName} |
| VMRule names | coeurli-argocd, coeurli-databases, … |
monitoring-scrapes/templates/vmrule-*.yaml |
:material-cog: global.cluster.name prefix |
| Longhorn backup target | s3://longhorn@<region>/, endpoint <s3-endpoint> |
longhorn Application helm.values, longhorn-s3-secret |
:material-cog: region ← global.backup.s3.region + :material-map-marker: endpoint (in the SealedSecret) |
| Admin / notification email | admin@coeur.li, cluster-digest@coeur.li, talos-cluster@coeur.li |
cert-manager issuer, victoria-metrics-k8s-stack, cluster-digest |
:material-cog: global.notifications.adminEmail + baseDomain |
| Git owner + host | your org, https://codeberg.org |
renovate/values.yaml, cluster-digest/values.yaml (the digest's issueRepo) |
:material-cog: global.git.{owner,platform,endpoint} |
| cluster-digest topology | coeurli, "3 CP + 6 workers", IP string |
cluster-digest/templates/configmap.yaml (docstring, SYSTEM_PROMPT, email subject) |
:material-cog: global.cluster.name + a topology value |
Network — the edge contract¶
These are physical LAN/WAN/firewall facts shared with the edge firewall, which is managed separately from the cluster. They are not extractable — you replace them with your own, and keep the NodePort numbers in lockstep with your firewall's DNAT rules.
| Fact | Ours | Where |
|---|---|---|
| LAN segment | 172.16.15.0/24 |
cilium-config/host-firewall.yaml, cilium/values.yaml, haproxy-ingress/values.yaml (proxy-protocol CIDR) |
| Cilium LB IP pool | single /32 in the LAN |
cilium/templates/lb-ip-pool.yaml |
| HAProxy publish IPs | 6 worker IPs + 2 OPNsense CARP peers | haproxy-ingress/values.yaml |
| etcd scrape targets | 3 CP IPs :2381 |
monitoring-scrapes/templates/etcd.yaml |
| Public WAN CIDR | <public-WAN-CIDR> (ens3) |
cilium/values.yaml |
| NodePort map | 30080/30443 (HTTP/S), 30025/30465/30587 (mail), 30993/30995 (IMAP/POP3S), 30478/30349 (TURN), 31443/31080 |
cilium-config/host-firewall.yaml, haproxy-ingress/values.yaml |
The NodePorts are an interface, not a preference
A downstream firewall must DNAT the same NodePort numbers (or you change them in both places). Treat this table as the contract between the cluster and your edge.
Layer 3 — Addons¶
Repo: addons-argocd. Besides the usual DNS (netbird., vault., id., imap.,
smtp. + apps.int), this layer has the least portable couplings in the whole
stack — because the NetBird mail-mesh pins cluster-assigned addresses by hand.
Pinned ClusterIPs + hand-rolled EndpointSlices + a Network UUID
These are values your cluster's CNI and NetBird server will assign differently — you cannot reuse ours, and each appears in several files that must stay in lockstep:
| Coupling | Value (yours will differ) | Where (must match across all) |
|---|---|---|
| IMAP stub ClusterIP → backend | <imap-stub-ClusterIP> → <dovecot-ClusterIP> |
netbird/templates/services-mail.yaml (Service + EndpointSlice), nb-resource-mail.yaml (address:) |
| SMTP stub ClusterIP → backend | <smtp-stub-ClusterIP> → <postfix-ext-ClusterIP> |
same files |
| Health-canary ClusterIP | <health-canary-ClusterIP> |
nb-resource-health.yaml (Service + EndpointSlice + NBResource) |
| NetBird Network UUID | <network-UUID> (mail-router) |
nb-resource-mail.yaml, nb-resource-health.yaml — re-discover after you create the Network |
The header comment in nb-resource-mail.yaml documents the UUID-lookup procedure.
If you don't need mail-over-NetBird, delete this plumbing entirely (see
Customise).
| Other couplings | Ours | Where | Replace with |
|---|---|---|---|
| OIDC clients | netbird-public, netbird-management, vaultwarden |
netbird/values.yaml, vaultwarden/values.yaml, create-keycloak-objects.sh |
your Keycloak client IDs |
| Keycloak realm slug | opendesk |
every id.coeur.li/realms/opendesk URL |
:material-cog: global.realm |
| Vaultwarden backup | endpoint <s3-endpoint>, region <region>, bucket vaultwarden |
global.backup.s3.* (endpoint/region), vaultwarden/values.yaml (bucket) |
:material-cog: global.backup.s3.* + :material-map-marker: bucket |
| Backup age recipient | committed public age key | vaultwarden/values.yaml |
:material-map-marker: regenerate your own keypair |
| NetBird object names | netbird-family, mail-router, group/policy names |
nb-*.yaml |
portable, but keep in 1:1 sync with your Keycloak groups |
Layer 4 — openDesk¶
Repo: opendesk-argocd. Own-chart couplings are straightforward; the valuable
(and hardest) category is the application content created inside the upstream
helmfile render.
| Own-chart couplings | Ours | Where | Replace with |
|---|---|---|---|
| openDesk domain + host map | coeur.li + 22 subdomains |
opendesk-certificates/values.yaml (cert SANs) |
:material-cog: global.dns.baseDomain + host map |
| Garage S3 host / region / buckets | objectstore.coeur.li, us-east-1, 7 service buckets |
garage/values-prod.yaml |
:material-cog: domain + :material-map-marker: buckets |
| Backing-store buckets | postgres-opendesk, mariadb-opendesk, garage (off-site) |
postgresql/, mariadb/, garage/ values-prod.yaml (buckets); global.backup.s3.endpoint |
:material-cog: endpoint ← global.backup.s3.endpoint + :material-map-marker: bucket names |
| ClusterIssuer | coeurli-issuer |
garage/templates/Ingress.yaml |
:material-cog: global.cluster.issuerName |
| TURN realm / host | coeur.li, turns.coeur.li |
coturn realm + certificate.dnsName (injected via the app-of-apps helm.values) |
:material-cog: global.dns.baseDomain |
Entwined — the openDesk upstream render
These are created by the upstream helmfile + Keycloak bootstrap. Never hand-edit them individually — they must stay mutually consistent. Change them through the render inputs (your domain + master password), then let the pipeline regenerate.
- 13 PostgreSQL databases:
postgres-opendesk,keycloak,keycloak_extensions,matrix,nextcloud,notes,openproject,nubus_authsession,guardianmanagementapi,notificationsapi,selfservice,xwiki,mautrix_whatsapp - 13 PostgreSQL users (DNS-label form):
keycloak-user,matrix-user,nextcloud-user, … — re-referenced asdatabase.<svc>.usernameoverrides; the two sides must match. - Matrix
server_name= the base domain (also the mautrix bridge permission keys). opendesk-matrixOIDC client (Synapse).- The Keycloak realm — never a literal in the repo; created by the upstream
opendesk-keycloak-bootstrap/ums-keycloak-bootstrapjobs.
Network (Layer 4)¶
Same edge-contract rules as Layer 2 — all :material-map-marker: site-specific:
- coturn pins node hostnames + node/WAN IPs (
node01/node02,172.16.15.200/201,<public-WAN>) and shared NodePorts30478/30349—coturn-fw{1,2}/values.yaml. - Mail NodePorts
30993/30995/30025/30587/30465—generate-argocd-objects.sh,values.yaml.gotmpl; your MX + firewall depend on these being stable. - WOPI/Collabora allow-CIDR
172.16.0.0/12—values.yaml.gotmpl.
Component toggles¶
Every optional component is gated by a flag in that repo's values/global.yaml, under
global.components:
The app-of-apps renders a component's ArgoCD Application (and, in platform-argocd, its namespace,
NetworkPolicies, and quota) only when its flag is true. Setting a flag to false prunes the
component and everything it manages. All flags default to true, so an existing cluster is
unaffected — the toggles are for composing a deployment that omits a component.
| Repo | Flag(s) | Gates |
|---|---|---|
| platform-argocd | opendesk |
the whole openDesk app layer — the opendesk / opendesk-addons / opendesk-prod / opendesk-argocd namespaces. Set false for a base cluster with no openDesk. |
| platform-argocd | postgresOperator / mariadbOperator / garageOperator |
the Percona Postgres, MariaDB, and Garage (S3) operators. Each is independent of opendesk (default true), so a different deployment can run any DB/storage engine for a non-openDesk component. Here postgres is shared (authentik / netbird / vaultwarden + openDesk) while mariadb + garage happen to serve only openDesk. |
| platform-argocd + addons-argocd | authentik / netbird / vaultwarden |
each component's namespace + operator (platform-argocd) and its workload + authentik blueprint entries (addons-argocd) |
| platform-argocd + opendesk-argocd | imapsync |
the imapsync IMAP-migration helper — its namespace (platform-argocd) and its workload (opendesk-argocd) |
| opendesk-argocd | directoryImporter |
the identity-federation importer (disable if you have no upstream IdP) |
| opendesk-argocd | mautrixWhatsapp |
the Matrix ↔ WhatsApp bridge |
authentik, netbird, and vaultwarden span both the platform-argocd and addons-argocd
instances (namespace/operator in platform-argocd, workload in addons-argocd); imapsync spans
platform-argocd (namespace) and opendesk-argocd (workload). Set each spanning flag the same in both
repos. The core operators (cert-manager, longhorn, sealed-secrets) are mandatory and have no toggle.
Prune safety: flipping a flag to
falsetriggers an ArgoCD prune. For a stateful component (databases, mail, storage) that is data loss. The toggles are meant for composing a new deployment that never had the component — not for removing one from a running cluster.
Identity & operational assumptions¶
Cross-cutting, mostly :material-map-marker: document-only — a different operator substitutes their own:
- Git owner
alpstackoncodeberg.org— already templated for the source repo (global.sourceRepo.url); sibling repo URLs + Renovate/digest config still literal. - Secrets vault — we use Psono; the bootstrap scripts reference it for master-password/DKIM/TURN custody. You use your own vault; only sealed secrets ever reach Git.
- Owners / emails —
admin@coeur.li, operator names (Adi, pesche), and a transientherzig.ccmailbox reference in the imapsync migration tool.
Next: Configuration values →