Skip to content

Architecture

The customer sheet from the previous page carries the values. This page shows where they fit in: the target deployment view, the layers that make it up and their dependencies, name resolution depending on where the request comes from, and the boundary between what a script sets and what a human has to set.

Overview

The first figure shows who talks to whom: three actors, each by a distinct route, and the internal layers they reach. The public user goes through the Cloudflare edge then the tunnel to Traefik; the internal agent goes through WireGuard to the internal DNS resolution; an external third party (a customs system, in the repository's example) reaches a microservice directly with a signed request, without going through Traefik or through the other two routes. It does not show the detailed path of a request nor what is checked at each step: that is the subject of the next figure. Nor does it show the physical placement of the machines, already seen on the previous page.

Who talks to whom: actors, exposure, cluster, data layer

Three routes enter the system, and none of the three looks like the other two: it is the third one, the external third party reaching a microservice directly, without going through Traefik, that breaks the idea of a single entry point and deserves to be spotted first.

The second figure follows a public request, step by step, with every point where it can stop: DNS resolution, filtering at the Cloudflare edge (blocking the administration console, rate limiting, cache), tunnel exit, checking the declared host, Traefik, the portal, then the .NET API through to its database. It also places, as a dashed line, the shortcut taken by a LAN or VPN workstation: its resolver returns the address of Traefik directly, going through neither the edge nor the tunnel. It does not show the request issued from a pod internal to the cluster, covered by the name resolution table further down.

The path of a public request and what is checked at each step

The dashed shortcut is the most common verification trap: a request sent from the LAN or the VPN skips the first two steps of the chain, so a test that succeeds from that network proves nothing about the public exposure. Verify from an outside network, never from the LAN or the VPN.

The six layers and their dependencies

The target installation is organised into six layers. Each depends on the one before it: installing a layer before the one it depends on produces a component that starts without being able to work, or that does not start at all. The next page details why this order cannot be permuted; this one sets out the list and the dependencies.

# Layer Contents Depends on
1 Talos Cluster bootstrap: control-plane, workers, kube-vip on the Talos side nothing, it is the foundation
2 Secrets Vault (secrets/vault.yaml, StatefulSet of 3 replicas in the vault namespace), External Secrets Operator, the ClusterSecretStore Talos only
3 Certificates cert-manager and the wildcard certificate Secrets, because the ClusterIssuer resolves its Cloudflare token through an ExternalSecret fed by Vault
4 Network, ingress and internal DNS Calico, kube-vip on the Kubernetes side, Traefik and its TLSStore, CoreDNS Certificates, because the TLSStore cannot serve a certificate that is not Ready
5 Data layer Databases and queues internal to the cluster, access to the external data layer Secrets, because the connection credentials are injected by ESO
6 Applications and exposure Keycloak and its realm, .NET microservices, Next.js portals, Cloudflare tunnel, VPN Data layer, Secrets, and the internal DNS for resolving *.${TV_DOMAIN}

The point that deserves detail is the move from layer 2 to layer 3: at first sight, the secrets and the certificates seem to reference each other, since Vault's own UI is exposed under the wildcard domain, by an Ingress laid down at layer 4. The loop does not close, however, because Vault never needs that Ingress to work: its Service vault.vault.svc:8200, in the vault namespace, is covered by kubectl kustomize . and resolves through Kubernetes' standard internal DNS as soon as Talos and the CNI are in place, without depending on layer 4's customised CoreDNS or on any certificate. It is this Service, reachable from the operator's workstation by kubectl port-forward with no public DNS and no certificate, that is used to unseal Vault and to write into it the Cloudflare token that the ClusterIssuer of layer 3 expects. Layer 4's Ingress only adds a more comfortable web access to an already working UI.

Name resolution: the split-horizon

The same name *.${TV_DOMAIN} does not resolve the same way depending on where the request comes from.

Origin Resolution By what
Inside the cluster to the Traefik service, ${TV_TRAEFIK_CLUSTERIP} CoreDNS, by a rewrite rule
On the VPN or the LAN to ${TV_VIP} the LAN DNS resolver, pushed to WireGuard clients or delegated by the site's DHCP
From the internet to the Cloudflare edge, never directly to the cluster three public records only (hub, portal, auth), via the Cloudflare tunnel

What is fixed, what is parameterisable

scripts/render-config.sh substitutes ten families of values in the repository's manifests. It announces their number itself at launch, on the line Substitutions actives. Outside that list, an operation is either fixed (the same at every customer), or to be set by hand, without any text substitution being able to do it.

Parameterisable by render-config.sh Set by hand by the operator
The domain ${TV_DOMAIN}, including its escaped form in the CoreDNS rule The Cloudflare zone and API token (Zone:DNS:Edit and Zone:Zone:Read) for the DNS-01 challenge
The kube-vip range ${TV_VIP_RANGE} The Vault keys that carry the OIDC issuer and the API URLs of the .NET services
The VIP ${TV_VIP} The Keycloak realm, its Valid Redirect URIs and its Web Origins
Traefik's ClusterIP ${TV_TRAEFIK_CLUSTERIP} Rebuilding the frontend images for the NEXT_PUBLIC_* variables, frozen at build time
The host of the application registry ${TV_REGISTRY} Publishing the eleven application images into the customer's registry
The address of the LAN DNS ${TV_LAN_DNS_IP} The Cloudflare tunnel UUID, to be replaced by hand after its creation
The address of the WireGuard concentrator ${TV_VPN_IP}
The ACME contact address ${TV_ACME_EMAIL} The name of kube-vip's network interface, vip_interface, frozen to ens34 in core/kube-vip/kube-vip-ds.yaml
The pod subnet ${TV_POD_SUBNET}, in the WireGuard tunnel configuration
The service subnet ${TV_SVC_SUBNET}, in the same configuration

Each image's tag follows a rule of its own, with no dedicated variable in the customer sheet: the repository's ten application images carry an immutable per-build tag, and keycloak-tradeval carries prod pinned by its digest, specific to the registry that built the image. A single variable applied to the whole set would build references that exist in no registry. Each image's tag is therefore set at the moment the operator publishes their own images, at the same time as their name.

${TV_CP_ENDPOINT} stays out of reach of render-config.sh for a different reason: it does not live in a manifest to be substituted, but in the Talos machine configuration, generated by talosctl. ${TV_POD_SUBNET} and ${TV_SVC_SUBNET} live in that same configuration, and in addition in core/vpn/peer-officer.template.conf, where the script substitutes them: both places must carry the same value, failing which the tunnel comes up and routes nothing, with no error. See Prerequisites.