diff --git a/services/platform/gateway/config/values-scaleway.yaml b/services/platform/gateway/config/values-scaleway.yaml index b78d267..c91956c 100644 --- a/services/platform/gateway/config/values-scaleway.yaml +++ b/services/platform/gateway/config/values-scaleway.yaml @@ -33,6 +33,7 @@ acmeEmail: nicolas.brieussel@gmail.com # namespace (same Scaleway DNS credentials external-dns already uses). scalewayCredentialsSecretName: scaleway-dns-credentials + # PreSync hook (templates/cert-restore-*.yaml): restores the ACME account key # + wildcardCertSecretName above from the latest Velero backup BEFORE this # chart's own ClusterIssuers/Gateway get created on a fresh cluster — see diff --git a/services/platform/secrets-sync/config/values.yaml b/services/platform/secrets-sync/config/values.yaml index 0a3acea..1399898 100644 --- a/services/platform/secrets-sync/config/values.yaml +++ b/services/platform/secrets-sync/config/values.yaml @@ -72,6 +72,7 @@ targets: secrets: - SCW_ACCESS_KEY - SCW_SECRET_KEY + - WG_CI_PRIVATE_KEY # This same round-trip (OpenBao -> ESO -> GitHub env secret) is also # meant to close the CI bootstrap loop for the infra repo's # 05-secrets//managed roots: each root's AppRole role_id/ @@ -79,8 +80,19 @@ targets: # here too, so CI can run `terraform apply` on those roots without a # human re-supplying credentials each time. Not yet wired for # 05-secrets/openbao/managed specifically (only SCW_ACCESS_KEY/ - # SCW_SECRET_KEY are merged today) — see infra repo's - # 05-secrets/openbao/README.md for the full pattern. + # SCW_SECRET_KEY/WG_CI_PRIVATE_KEY are merged today) — see infra + # repo's 05-secrets/openbao/README.md for the full pattern. + # + # WG_CI_PRIVATE_KEY: CI's own WireGuard peer key (infra repo's + # 04-vpn/wireguard's peer_private_keys["ci-github-actions"] + # output) — brings up the tunnel to OpenBao's ClusterIP before + # `terraform plan/apply` on 05-secrets/openbao/{bootstrap,managed}. + # Merged into THIS same kv/apps/secrets-sync/github/infrastructure-scaleway + # object (not a separate kv/apps/wireguard/* one — nothing but this + # target's own pipeline would ever read that): supplied straight into + # 05-secrets/openbao/managed's local.auto.tfvars from + # 04-vpn/wireguard's output, same as every other + # externally-issued credential on that root. # Visibilité des secrets d'organisation créés : "all" ou "private". orgSecretVisibility: private diff --git a/services/platform/wireguard/applications/scaleway/config.app.yaml b/services/platform/wireguard/applications/scaleway/config.app.yaml new file mode 100644 index 0000000..e4cea20 --- /dev/null +++ b/services/platform/wireguard/applications/scaleway/config.app.yaml @@ -0,0 +1,9 @@ +# Wave 5: needs wireguard-init's wave-3 Secret (the tunnel server's private +# key) to exist first. No Gateway dependency (NodePort, not routed through +# it — see templates/service.yaml) — wave 5 kept anyway for a comfortable +# margin past wireguard-init, not because it's required here. +name: wireguard-config +namespace: wireguard +chartPath: services/platform/wireguard/config +valueFile: values-scaleway.yaml +syncWave: "5" diff --git a/services/platform/wireguard/applications/scaleway/init.app.yaml b/services/platform/wireguard/applications/scaleway/init.app.yaml new file mode 100644 index 0000000..a98dc3c --- /dev/null +++ b/services/platform/wireguard/applications/scaleway/init.app.yaml @@ -0,0 +1,5 @@ +name: wireguard-init +namespace: wireguard +chartPath: services/platform/wireguard/init +valueFile: values-scaleway.yaml +syncWave: "3" diff --git a/services/platform/wireguard/config/Chart.yaml b/services/platform/wireguard/config/Chart.yaml new file mode 100644 index 0000000..6d45950 --- /dev/null +++ b/services/platform/wireguard/config/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: wireguard-config +description: Self-hosted WireGuard tunnel server, fronted by the shared Envoy Gateway, giving Terraform's vault provider a non-public path to OpenBao's ClusterIP +type: application +version: 0.1.0 diff --git a/services/platform/wireguard/config/templates/configmap.yaml b/services/platform/wireguard/config/templates/configmap.yaml new file mode 100644 index 0000000..4b04016 --- /dev/null +++ b/services/platform/wireguard/config/templates/configmap.yaml @@ -0,0 +1,27 @@ +# Non-secret half of wg0.conf ([Interface] only). The private key comes +# from the Secret services/platform/wireguard/init materializes; [Peer] +# blocks come from that same init chart's wireguard-peers Secret — both +# combined by an initContainer at pod start (see deployment.yaml), never +# by Helm/ArgoCD, since ArgoCD's rendering has no access to a Secret's +# live content. +apiVersion: v1 +kind: ConfigMap +metadata: + name: wireguard-conf-template + namespace: {{ .Values.namespace }} +data: + wg0.conf.tmpl: | + [Interface] + Address = {{ .Values.server.address }} + ListenPort = {{ .Values.server.port }} + PrivateKey = __PRIVATE_KEY__ + # No PostUp/PostDown NAT+forwarding rules on purpose (see deployment.yaml's + # proxy-gateway container) — this pod isn't a router for the wider cluster, it terminates the + # tunnel and re-originates plain outbound connections via userspace proxies. + # That needs no net.ipv4.ip_forward/src_valid_mark: tried the kernel-routing + # approach first, but Kapsule's kubelet doesn't allowlist either sysctl and + # Scaleway's own kubelet_args API rejects widening that allowlist for this + # k8s version anyway — and even where it's technically available, an + # unsafe-sysctls allowlist is a cluster-wide relaxation for one workload's + # benefit, worth avoiding on its own merits, not just because it was + # blocked here. diff --git a/services/platform/wireguard/config/templates/deployment.yaml b/services/platform/wireguard/config/templates/deployment.yaml new file mode 100644 index 0000000..2d11daf --- /dev/null +++ b/services/platform/wireguard/config/templates/deployment.yaml @@ -0,0 +1,158 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wireguard + namespace: {{ .Values.namespace }} +spec: + replicas: 1 + strategy: + # A second replica during a rollout would mean two pods both claiming + # the same peer set with independent state — recreate rather than + # risk that, a short tunnel blip during a rollout is fine. + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: wireguard + template: + metadata: + labels: + app.kubernetes.io/name: wireguard + spec: + serviceAccountName: wireguard + initContainers: + # Combines the non-secret template (configmap.yaml) with the private + # key from services/platform/wireguard/init's Secret into the actual + # wg0.conf the server container reads — this has to happen at pod + # start, not at Helm render time, since ArgoCD never sees the + # Secret's live content. + - name: render-wg-conf + image: alpine:3.22 + command: ["sh", "-c"] + args: + - | + set -eu + apk add --no-cache jq + PRIVATE_KEY=$(cat /secrets/{{ .Values.server.privateKeySecretKey }}) + sed "s|__PRIVATE_KEY__|$PRIVATE_KEY|" /template/wg0.conf.tmpl > /config/wg_confs/wg0.conf + # One file per peer (the wireguard-peers Secret mounted below), + # each containing that peer's {publicKey, allowedIPs} as JSON — + # see infra repo's 05-secrets/openbao/managed/main.tf for how + # that Secret's content is shaped. + for f in /peers/*; do + name=$(basename "$f") + pubkey=$(jq -r '.publicKey' "$f") + allowedips=$(jq -r '.allowedIPs' "$f") + printf "\n# %s\n[Peer]\nPublicKey = %s\nAllowedIPs = %s\n" "$name" "$pubkey" "$allowedips" >> /config/wg_confs/wg0.conf + done + chmod 600 /config/wg_confs/wg0.conf + volumeMounts: + - name: conf-template + mountPath: /template + readOnly: true + - name: server-key + mountPath: /secrets + readOnly: true + - name: peers + mountPath: /peers + readOnly: true + - name: wg-confs + mountPath: /config/wg_confs + containers: + - name: wireguard + image: "{{ .Values.server.image.repository }}:{{ .Values.server.image.tag }}" + securityContext: + capabilities: + add: ["NET_ADMIN"] + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + # Custom wg_confs/wg0.conf is supplied directly (mounted below) + # — this disables the image's own key/peer auto-generation, + # since Terraform (infra repo's 04-vpn/wireguard) already + # owns that. + ports: + - name: wireguard + containerPort: {{ .Values.server.port }} + protocol: UDP + volumeMounts: + - name: wg-confs + mountPath: /config/wg_confs + # One sidecar per proxyTargets entry — each terminates the tunnel + # connection and re-originates a plain outbound TCP connection to + # its own target. Userspace proxying, not kernel routing, so no + # ip_forward/src_valid_mark, no iptables NAT, no capabilities + # beyond the wireguard container's own NET_ADMIN above (needed to + # create wg0 at all, unrelated to this). Each listens on every + # interface in this pod's netns, including wg0 once the tunnel is + # up — a peer with the right AllowedIPs dials this pod's tunnel + # address on that target's port directly. + {{- range $name, $proxyTarget := .Values.proxyTargets }} + - name: proxy-{{ $name }} + image: alpine:3.22 + command: ["sh", "-c"] + args: + - | + set -eu + apk add --no-cache socat + exec socat TCP-LISTEN:{{ $proxyTarget.port }},fork,reuseaddr TCP:{{ $proxyTarget.target }} + {{- end }} + # Raw TCP passthrough to Envoy Gateway's own real Service — SNI/Host + # travel untouched inside the TLS ClientHello, so Envoy does its + # normal SNI-based routing to whichever app (Grafana, ArgoCD, Dex, + # OpenBao's UI, ...) exactly as it does for public traffic. No + # per-app sidecar, no TLS termination here, and it covers every + # app behind the Gateway automatically — this is what makes OIDC + # logins (state cookie tied to the app's one real origin) work + # over the tunnel: from every app's point of view this traffic IS + # indistinguishable from the public path, just arriving over a + # different network. Service name is resolved at startup (RBAC in + # rbac.yaml) rather than hardcoded — Envoy Gateway generates it + # per-Gateway-object and it isn't guaranteed stable across a + # recreate. + - name: proxy-gateway + image: alpine/kubectl:1.35.3 + command: ["sh", "-c"] + args: + - | + set -eu + apk add --no-cache socat + SVC=$(kubectl get svc -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=scaleway-gateway -o jsonpath='{.items[0].metadata.name}') + TARGET="${SVC}.envoy-gateway-system.svc.cluster.local" + echo "proxying to Envoy Gateway Service: ${TARGET}" + socat TCP-LISTEN:443,fork,reuseaddr TCP:${TARGET}:443 & + socat TCP-LISTEN:80,fork,reuseaddr TCP:${TARGET}:80 & + wait + # Split DNS: answers *.scalepack.fr with this pod's own tunnel + # address (server.address, stripped of its CIDR suffix) and + # forwards everything else upstream. Peers get this pushed via + # their wg-quick config's DNS setting (infra repo's + # 04-vpn/wireguard/main.tf) — while the tunnel is up, every + # *.scalepack.fr hostname resolves to the tunnel automatically, no + # /etc/hosts editing, and it reverts the moment the tunnel goes + # down (scoped to the interface's own DNS config, not a standing + # file edit). + - name: dns + image: alpine:3.22 + command: ["sh", "-c"] + args: + - | + set -eu + apk add --no-cache dnsmasq + exec dnsmasq --no-daemon --no-resolv \ + --address=/{{ .Values.dnsZone }}/{{ (splitList "/" .Values.server.address) | first }} \ + --server=1.1.1.1 --server=8.8.8.8 \ + --port=53 + volumes: + - name: conf-template + configMap: + name: wireguard-conf-template + - name: server-key + secret: + secretName: {{ .Values.server.privateKeySecretName }} + - name: peers + secret: + secretName: {{ .Values.server.peersSecretName }} + - name: wg-confs + emptyDir: {} diff --git a/services/platform/wireguard/config/templates/networkpolicy.yaml b/services/platform/wireguard/config/templates/networkpolicy.yaml new file mode 100644 index 0000000..1ccc9ab --- /dev/null +++ b/services/platform/wireguard/config/templates/networkpolicy.yaml @@ -0,0 +1,24 @@ +# Explicit least-privilege ingress for the one pod in this cluster reachable +# from the raw internet via a node's public IP (see service.yaml) — a +# default Kubernetes stance of "whatever the pod spec exposes is reachable" +# isn't good enough for a pod sitting on that entry point. This is the +# enforced contract: exactly UDP/{{ .Values.server.port }}, from anywhere (peers roam, can't be +# source-IP-scoped), nothing else — so a future change that adds another +# container port to this pod doesn't silently become internet-reachable. +# Enforced by Cilium (this cluster's CNI), which programs NodePort-DNAT'd +# traffic through the same policy engine as any other ingress. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: wireguard + namespace: {{ .Values.namespace }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: wireguard + policyTypes: + - Ingress + ingress: + - ports: + - protocol: UDP + port: {{ .Values.server.port }} diff --git a/services/platform/wireguard/config/templates/rbac.yaml b/services/platform/wireguard/config/templates/rbac.yaml new file mode 100644 index 0000000..3b0a1e0 --- /dev/null +++ b/services/platform/wireguard/config/templates/rbac.yaml @@ -0,0 +1,38 @@ +# Dedicated ServiceAccount (not "default") so the RBAC grant below is scoped +# to exactly this pod, nothing else in the wireguard namespace. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: wireguard + namespace: {{ .Values.namespace }} +--- +# get/list on Services in envoy-gateway-system only — lets the +# proxy-gateway sidecar (deployment.yaml) resolve Envoy Gateway's real +# Service name at startup instead of hardcoding it. That name is +# auto-generated per Gateway object (envoy-gateway-scaleway-gateway-) +# and not guaranteed stable across a Gateway recreate — the same class of +# problem wg_endpoint had, solved the same way: discover it live instead of +# tracking one more hardcoded reference. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: wireguard-read-gateway-svc + namespace: envoy-gateway-system +rules: + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: wireguard-read-gateway-svc + namespace: envoy-gateway-system +subjects: + - kind: ServiceAccount + name: wireguard + namespace: {{ .Values.namespace }} +roleRef: + kind: Role + name: wireguard-read-gateway-svc + apiGroup: rbac.authorization.k8s.io diff --git a/services/platform/wireguard/config/templates/service.yaml b/services/platform/wireguard/config/templates/service.yaml new file mode 100644 index 0000000..6ebc3b9 --- /dev/null +++ b/services/platform/wireguard/config/templates/service.yaml @@ -0,0 +1,46 @@ +# NodePort, not routed through the shared Gateway — confirmed live 2026-08-09 +# that Scaleway's cloud-controller-manager silently drops any non-TCP +# Service port when building the LB (scaleway-cloud-controller-manager's +# loadbalancers.go: `if port.Protocol != v1.ProtocolTCP { skip }`), so a +# UDPRoute on the Gateway's LB was never reachable — zero datagrams ever +# arrived at Envoy despite every Gateway API resource reporting Accepted/ +# Programmed. Kapsule nodes already carry individual public IPs, so this +# exposes the tunnel directly on those instead — a genuinely new entry +# point, hardened accordingly: +# - externalTrafficPolicy: Local — only node(s) actually running this +# pod accept traffic on this port at all; every other node in the pool +# stays fully closed on it. Also preserves the real client source IP +# (no extra SNAT hop) and avoids Kubernetes' cross-node NodePort +# redirect entirely. +# - templates/networkpolicy.yaml is the explicit least-privilege +# complement: ingress to this pod is enforced at exactly UDP/{{ .Values.server.port }}, +# nothing else, regardless of what the pod spec happens to expose. +# - WireGuard's own protocol does the rest: an unauthenticated packet is +# silently dropped, no response, no banner — a port scan can't tell +# this apart from a closed port. +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.server.serviceName }} + namespace: {{ .Values.namespace }} + annotations: + # external-dns's `service` source has first-class NodePort support: for + # externalTrafficPolicy: Local (above), it resolves the ExternalIP of + # whichever node(s) currently have a live pod backing this Service + # (source/service.go's nodesExternalTrafficPolicyTypeLocal) and keeps + # this record pointed there — no custom script, this is exactly the + # same external-dns already running for every other *.scalepack.fr + # hostname (services/platform/external-dns). Peers dial this hostname, + # never a raw node IP that changes on every reschedule/cluster rebuild. + external-dns.alpha.kubernetes.io/hostname: {{ .Values.server.hostname }} +spec: + type: NodePort + externalTrafficPolicy: Local + selector: + app.kubernetes.io/name: wireguard + ports: + - name: wireguard + port: {{ .Values.server.port }} + targetPort: {{ .Values.server.port }} + nodePort: {{ .Values.server.nodePort }} + protocol: UDP diff --git a/services/platform/wireguard/config/values-scaleway.yaml b/services/platform/wireguard/config/values-scaleway.yaml new file mode 100644 index 0000000..46e6d50 --- /dev/null +++ b/services/platform/wireguard/config/values-scaleway.yaml @@ -0,0 +1,6 @@ +# No overrides needed — the peer list used to be hardcoded here, replaced +# by services/platform/wireguard/init's wireguard-peers Secret (synced from +# OpenBao, itself sourced from the infra repo's 04-vpn/wireguard state). +# Kept as an empty file so this chart follows the same +# applications/scaleway -> values-scaleway.yaml convention as every other +# chart in this repo, in case a real override shows up later. diff --git a/services/platform/wireguard/config/values.yaml b/services/platform/wireguard/config/values.yaml new file mode 100644 index 0000000..718fc90 --- /dev/null +++ b/services/platform/wireguard/config/values.yaml @@ -0,0 +1,93 @@ +# A self-hosted WireGuard tunnel so infra repo's 05-secrets/openbao's vault +# provider (local dev + CI) can reach OpenBao instead of the public gateway +# route (openbao.scalepack.fr) — that route stays public for a different, +# unrelated reason: OpenBao's own native OIDC/UI login for humans +# (services/platform/openbao/config), which this domain doesn't touch. No +# third-party control plane (Tailscale evaluated, ruled out) — see infra +# repo's 04-vpn/wireguard/README.md for the full design and where every +# key in this chart's values actually comes from. +# +# This pod terminates the tunnel and proxies to each target in +# proxyTargets below at the application layer (see templates/deployment.yaml, +# one sidecar container per entry) rather than routing/NAT-ing at the kernel +# layer — a first attempt used net.ipv4.ip_forward + iptables MASQUERADE, +# which needs the node pool's kubelet to allowlist those as "unsafe +# sysctls". Confirmed live 2026-08-09 that's a dead end twice over: +# Scaleway's kubelet_args API rejects widening that allowlist for this +# cluster's k8s version at all, and even where it is available, it's a +# cluster-wide relaxation for one workload's benefit — worth avoiding on +# its own merits. The proxy needs none of that: a peer's traffic terminates +# locally at this pod's own tunnel address, so there's no forwarding +# decision for the kernel to make at all. +# +# Exposed as a NodePort directly on a Kapsule node's public IP, not through +# the shared Gateway — confirmed live 2026-08-09 that Scaleway's LB Service +# integration silently drops non-TCP ports, so a UDPRoute there is never +# actually reachable no matter how correctly Envoy Gateway itself is +# configured. See templates/service.yaml and templates/networkpolicy.yaml +# for how that new entry point is hardened (externalTrafficPolicy: Local + +# explicit least-privilege NetworkPolicy, on top of WireGuard's own +# silent-drop-on-auth-failure behavior). +# +# OIDC-gated web UIs (Grafana, ArgoCD, ...) don't work through a per-app +# proxyTargets entry: those apps generate their own redirect_uri from a +# single hardcoded root_url, so a login started via a tunnel-specific +# address (different host/port than that root_url) can never complete — +# confirmed live 2026-08-09, Grafana's "Missing saved oauth state". Fixed +# via templates/deployment.yaml's proxy-gateway sidecar (raw TCP passthrough +# to Envoy Gateway's real Service, SNI/Host untouched — indistinguishable +# from public traffic to every app behind it) plus the dns sidecar (split-DNS: +# *.scalepack.fr resolves to this pod's tunnel address only while the tunnel +# is up, pushed to peers via their wg-quick config's DNS setting). +# +# proxyTargets (below) started out also carrying a dedicated openbao:8200 +# entry for Terraform's vault provider, reasoned as "a machine credential +# doesn't need Gateway/TLS in its path" — that reasoning didn't actually +# hold up (OpenBao's HTTPRoute has no path restriction, so +# /v1/auth/approle/login routes through proxy-gateway exactly like the UI +# does; "no public listener" was already satisfied by both paths equally). +# Removed 2026-08-10 to test consolidating everything onto proxy-gateway — +# see infra repo's 05-secrets/openbao/{bootstrap,managed}/version.tf. +namespace: wireguard + +# The domain the dns sidecar answers for (see templates/deployment.yaml) — +# every *.scalepack.fr hostname resolves to server.address while the +# tunnel is up. +dnsZone: scalepack.fr + +server: + port: 51820 + # Fixed rather than letting Kubernetes assign one from the NodePort range + # at random — peers' Endpoint needs a port that doesn't change if this + # Service is ever recreated. Must stay inside the cluster's configured + # NodePort range (default 30000-32767). + nodePort: 30820 + # external-dns keeps this pointed at whichever node currently hosts the + # pod (see templates/service.yaml's annotation) — peers dial this, never + # a raw IP. + hostname: wg.scalepack.fr + # WireGuard's own point-to-point overlay subnet — unrelated to the + # cluster's pod/Service CIDR. The server sits at .1; each peer below gets + # its own /32 out of this range as its allowedIPs. + address: 10.100.0.1/24 + image: + repository: lscr.io/linuxserver/wireguard + tag: "1.0.20260223" + # Secrets materialized by services/platform/wireguard/init (ExternalSecrets + # from OpenBao's kv/apps/wireguard/server-key and kv/apps/wireguard/peers + # respectively) — nothing about the tunnel's keys or peer list is + # hardcoded in this repo, all of it flows from the infra repo's + # 04-vpn/wireguard through OpenBao (see that init chart's values.yaml). + privateKeySecretName: wireguard-server-key + privateKeySecretKey: private-key + peersSecretName: wireguard-peers + serviceName: wireguard + +# name -> {port, target}. One sidecar container + Service port per entry +# (see templates/deployment.yaml, templates/service.yaml) — a peer dials +# this pod's tunnel address on `port` and gets proxied to `target` +# (host:port, resolved by this pod's normal cluster DNS at connection time, +# so no ClusterIP-stability question to worry about). Empty for now — see +# the header comment above; add an entry back if something genuinely needs +# a dedicated non-Gateway path. +proxyTargets: {} diff --git a/services/platform/wireguard/init/Chart.yaml b/services/platform/wireguard/init/Chart.yaml new file mode 100644 index 0000000..55d66a6 --- /dev/null +++ b/services/platform/wireguard/init/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: wireguard-init +description: Materializes the WireGuard tunnel server's private key from OpenBao into the wireguard namespace +type: application +version: 0.1.0 diff --git a/services/platform/wireguard/init/templates/externalsecret-peers.yaml b/services/platform/wireguard/init/templates/externalsecret-peers.yaml new file mode 100644 index 0000000..58b2e14 --- /dev/null +++ b/services/platform/wireguard/init/templates/externalsecret-peers.yaml @@ -0,0 +1,23 @@ +# Not actually secret content, but same OpenBao -> ESO -> Secret pattern as +# every other value pulled into this cluster — see values.yaml. Each key in +# kv/apps/wireguard/peers becomes its own key in the resulting Secret, +# valued as a JSON blob ({publicKey, allowedIPs}) — mounted as a volume in +# services/platform/wireguard/config's Deployment, one file per peer. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ .Values.peers.secretName }} + namespace: {{ .Values.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + refreshInterval: {{ .Values.refreshInterval | quote }} + secretStoreRef: + name: {{ .Values.clusterSecretStoreName }} + kind: ClusterSecretStore + target: + name: {{ .Values.peers.secretName }} + creationPolicy: Owner + dataFrom: + - extract: + key: {{ .Values.peers.openbaoPath }} diff --git a/services/platform/wireguard/init/templates/externalsecret-server-key.yaml b/services/platform/wireguard/init/templates/externalsecret-server-key.yaml new file mode 100644 index 0000000..2fda133 --- /dev/null +++ b/services/platform/wireguard/init/templates/externalsecret-server-key.yaml @@ -0,0 +1,20 @@ +# Mirrors services/platform/dex/init's templates/externalsecret.yaml exactly — +# same OpenBao -> ESO -> Secret pattern every app's init chart uses. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ .Values.serverKey.secretName }} + namespace: {{ .Values.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + refreshInterval: {{ .Values.refreshInterval | quote }} + secretStoreRef: + name: {{ .Values.clusterSecretStoreName }} + kind: ClusterSecretStore + target: + name: {{ .Values.serverKey.secretName }} + creationPolicy: Owner + dataFrom: + - extract: + key: {{ .Values.serverKey.openbaoPath }} diff --git a/services/platform/wireguard/init/values-scaleway.yaml b/services/platform/wireguard/init/values-scaleway.yaml new file mode 100644 index 0000000..52010ee --- /dev/null +++ b/services/platform/wireguard/init/values-scaleway.yaml @@ -0,0 +1,4 @@ +# No overrides needed — base values.yaml already targets this environment's +# only current use (the Scaleway cluster's OpenBao tunnel). Kept as an empty +# file so this chart follows the same applications/scaleway -> values-scaleway.yaml +# convention as every other init chart, in case a divergence shows up later. diff --git a/services/platform/wireguard/init/values.yaml b/services/platform/wireguard/init/values.yaml new file mode 100644 index 0000000..2a647f8 --- /dev/null +++ b/services/platform/wireguard/init/values.yaml @@ -0,0 +1,26 @@ +# Pulls WireGuard state from OpenBao into Secrets that +# services/platform/wireguard/config's Deployment mounts — nothing about +# the tunnel is hardcoded in this repo, all of it is minted in the infra +# repo's 04-vpn/wireguard and routed through OpenBao like every other +# cross-repo value (05-secrets/openbao/managed, see that root's README for +# the full chain). +namespace: wireguard + +# ClusterSecretStore declared by the openbao-init chart. +clusterSecretStoreName: openbao + +refreshInterval: 1h + +# The tunnel server's own private key — sensitive, its own object since +# nothing else reads it. +serverKey: + openbaoPath: apps/wireguard/server-key + secretName: wireguard-server-key + +# name -> {publicKey, allowedIPs}. Not actually secret, but routed through +# the same pipe anyway — no cross-repo copy-paste for this either. See +# templates/../config/templates/deployment.yaml for how this Secret gets +# turned into wg0.conf's [Peer] blocks. +peers: + openbaoPath: apps/wireguard/peers + secretName: wireguard-peers