Skip to content

Commit 4218e4c

Browse files
committed
feat(ingress): issue shared DNS-01 wildcard on the publishing controller
When the platform signals shared wildcard issuance via _cluster.wildcard-issue, the publishing controller (Release.Namespace == expose-ingress) renders one cert-manager Certificate for <root-host> + *.<root-host>. The resulting Secret is served as the controller's default SSL certificate and the existing wildcard-secret-name consumption path makes the system services drop per-host ACME. The Certificate uses the DNS-01 ClusterIssuer that cert-manager-issuers already renders in dns01 mode, so no per-tenant Issuer is minted. Only the publishing controller renders it — the Secret must be same-namespace for ingress-nginx to read it. Part of #2811. Signed-off-by: Aleksei Sviridkin <[email protected]>
1 parent 34563ce commit 4218e4c

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
2+
{{- /* toString normalises the _cluster value whether it arrives as the
3+
string "true" (Flux valuesFrom Secret, the real path) or as a bool
4+
(helm --set), mirroring the proxy-protocol handling in
5+
nginx-ingress.yaml. */ -}}
6+
{{- $wildcardIssue := (index .Values._cluster "wildcard-issue") | default "false" | toString }}
7+
{{- $wildcardSecret := (index .Values._cluster "wildcard-secret-name") | default "" }}
8+
{{- $rootHost := (index .Values._cluster "root-host") | default "" }}
9+
{{- $issuerName := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
10+
{{- /*
11+
Shared ACME wildcard for the ingress-nginx path (WS2 / #2400). When the
12+
platform decides to issue a wildcard (_cluster.wildcard-issue=true,
13+
computed in core/platform apps.yaml only on the solver=dns01 ingress
14+
path with gateway.enabled=false), the publishing tenant's ingress
15+
controller renders ONE DNS-01 Certificate for <root-host> +
16+
*.<root-host>. ingress-nginx serves the resulting Secret as its
17+
--default-ssl-certificate (see nginx-ingress.yaml), so the system
18+
services drop their per-host cert-manager annotation — matching the
19+
Gateway API path, which issues a per-apex wildcard via the TenantGateway
20+
controller in dns01 mode.
21+
22+
Only the publishing controller (Release.Namespace == expose-ingress)
23+
renders it: the Secret must live in that namespace for ingress-nginx to
24+
read it, and child-tenant controllers cannot point --default-ssl-certificate
25+
at another namespace. The reference is therefore always same-namespace,
26+
the same constraint the operator-supplied wildcard path observes.
27+
28+
The ClusterIssuer named here (issuer-name, letsencrypt-prod by default)
29+
already carries the DNS-01 solver from packages/system/cert-manager-issuers
30+
whenever solver=dns01, so no per-tenant Issuer is minted on this path.
31+
*/}}
32+
{{- if and (eq $wildcardIssue "true") (eq .Release.Namespace $exposeIngress) }}
33+
{{- if not $wildcardSecret }}
34+
{{- fail "packages/extra/ingress: _cluster.wildcard-issue=true but _cluster.wildcard-secret-name is empty — the platform must supply the target Secret name." }}
35+
{{- end }}
36+
{{- if not $rootHost }}
37+
{{- fail "packages/extra/ingress: _cluster.wildcard-issue=true but _cluster.root-host is empty — cannot build the wildcard dnsNames." }}
38+
{{- end }}
39+
apiVersion: cert-manager.io/v1
40+
kind: Certificate
41+
metadata:
42+
name: cozystack-wildcard
43+
spec:
44+
secretName: {{ $wildcardSecret | quote }}
45+
issuerRef:
46+
kind: ClusterIssuer
47+
name: {{ $issuerName | quote }}
48+
dnsNames:
49+
- {{ $rootHost | quote }}
50+
- {{ printf "*.%s" $rootHost | quote }}
51+
{{- end }}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
suite: ingress-nginx ACME wildcard Certificate
2+
3+
templates:
4+
- templates/wildcard-certificate.yaml
5+
6+
release:
7+
name: ingress
8+
namespace: tenant-root
9+
10+
# WS2 / #2400: when the platform decides to issue a shared wildcard on the
11+
# ingress-nginx path (_cluster.wildcard-issue=true, computed in
12+
# core/platform apps.yaml only for solver=dns01 + gateway disabled), the
13+
# publishing tenant's ingress controller renders ONE DNS-01 Certificate
14+
# for <root-host> + *.<root-host>. ingress-nginx serves the resulting
15+
# Secret as its default SSL certificate, so per-host ACME is unnecessary.
16+
# Only the publishing controller (Release.Namespace == expose-ingress)
17+
# renders it — the Secret must live in that namespace and child-tenant
18+
# controllers cannot read it. The ClusterIssuer named here already carries
19+
# the DNS-01 solver whenever solver=dns01.
20+
21+
tests:
22+
- it: renders a DNS-01 wildcard Certificate on the publishing controller
23+
set:
24+
_cluster:
25+
expose-ingress: tenant-root
26+
wildcard-issue: "true"
27+
wildcard-secret-name: cozystack-wildcard-tls
28+
root-host: example.org
29+
issuer-name: letsencrypt-prod
30+
asserts:
31+
- hasDocuments:
32+
count: 1
33+
- equal:
34+
path: kind
35+
value: Certificate
36+
- equal:
37+
path: metadata.name
38+
value: cozystack-wildcard
39+
- equal:
40+
path: spec.secretName
41+
value: cozystack-wildcard-tls
42+
- equal:
43+
path: spec.issuerRef.kind
44+
value: ClusterIssuer
45+
- equal:
46+
path: spec.issuerRef.name
47+
value: letsencrypt-prod
48+
- equal:
49+
path: spec.dnsNames
50+
value:
51+
- example.org
52+
- "*.example.org"
53+
54+
- it: renders nothing when wildcard-issue is not set
55+
set:
56+
_cluster:
57+
expose-ingress: tenant-root
58+
wildcard-secret-name: ""
59+
root-host: example.org
60+
asserts:
61+
- hasDocuments:
62+
count: 0
63+
64+
- it: does not render on a non-publishing tenant controller
65+
release:
66+
name: ingress
67+
namespace: tenant-foo
68+
set:
69+
_cluster:
70+
expose-ingress: tenant-root
71+
wildcard-issue: "true"
72+
wildcard-secret-name: cozystack-wildcard-tls
73+
root-host: example.org
74+
asserts:
75+
- hasDocuments:
76+
count: 0
77+
78+
- it: honors a custom issuer name
79+
set:
80+
_cluster:
81+
expose-ingress: tenant-root
82+
wildcard-issue: "true"
83+
wildcard-secret-name: cozystack-wildcard-tls
84+
root-host: example.org
85+
issuer-name: letsencrypt-stage
86+
asserts:
87+
- equal:
88+
path: spec.issuerRef.name
89+
value: letsencrypt-stage
90+
91+
# Fail-fast guards: platform-internal invariants that should never trip,
92+
# but must fail loudly (not render a Certificate pointing at a
93+
# nonexistent Secret or with empty dnsNames) if apps.yaml ever regresses
94+
# and sets wildcard-issue=true without the keys it implies.
95+
- it: fails when wildcard-issue is true but the secret name is empty
96+
set:
97+
_cluster:
98+
expose-ingress: tenant-root
99+
wildcard-issue: "true"
100+
wildcard-secret-name: ""
101+
root-host: example.org
102+
asserts:
103+
- failedTemplate:
104+
errorMessage: "packages/extra/ingress: _cluster.wildcard-issue=true but _cluster.wildcard-secret-name is empty — the platform must supply the target Secret name."
105+
106+
- it: fails when wildcard-issue is true but root-host is empty
107+
set:
108+
_cluster:
109+
expose-ingress: tenant-root
110+
wildcard-issue: "true"
111+
wildcard-secret-name: cozystack-wildcard-tls
112+
root-host: ""
113+
asserts:
114+
- failedTemplate:
115+
errorMessage: "packages/extra/ingress: _cluster.wildcard-issue=true but _cluster.root-host is empty — cannot build the wildcard dnsNames."

0 commit comments

Comments
 (0)