Skip to content

Commit d4df6ae

Browse files
fix(nginx_gateway_fabric): restore bootstrap TLS secrets (break cert deadlock)
Moving HTTPS listeners into ListenerSets exposed a deadlock: the listener is rejected (InvalidCertificateRef) until its secret exists, but the cert-manager gateway-shim only issues for an accepted listener -> the real cert is never created. Restore the self-signed bootstrap secret (byte-identical to the prior module) so the listener is valid and accepted, then cert-manager overwrites it once HTTP-01 succeeds. Pair with cert_manager enableCertificateOwnerRef for clean teardown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 026672d commit d4df6ae

1 file changed

Lines changed: 113 additions & 0 deletions

File tree

nginx_gateway_fabric/main.tf

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,16 @@ locals {
174174
local.gateway_api_crd_labels
175175
)
176176

177+
# Domains that need bootstrap TLS certificates so the HTTPS listener is valid before
178+
# cert-manager issues. Without a placeholder secret the listener is rejected
179+
# (InvalidCertificateRef) and never accepted, so the gateway-shim never issues the
180+
# real cert — a deadlock. cert-manager overwrites this secret once HTTP-01 succeeds.
181+
bootstrap_tls_domains = var.external_tls_termination ? {} : {
182+
for domain_key, domain in local.domains :
183+
domain_key => domain
184+
if can(domain.domain) && lookup(domain, "certificate_reference", "") == ""
185+
}
186+
177187
# Domains that need cert-manager to issue certificates
178188
# Only domains WITHOUT certificate_reference
179189
# Not needed when TLS is terminated externally
@@ -1037,6 +1047,104 @@ locals {
10371047
)
10381048
}
10391049

1050+
# Bootstrap TLS for the base domain — placeholder self-signed cert so the HTTPS
1051+
# listener is valid and accepted before cert-manager issues the real cert (breaks
1052+
# the listener-invalid / cert-not-issued deadlock). cert-manager overwrites it.
1053+
resource "tls_private_key" "bootstrap" {
1054+
for_each = local.bootstrap_tls_domains
1055+
algorithm = "RSA"
1056+
rsa_bits = 2048
1057+
}
1058+
1059+
resource "tls_self_signed_cert" "bootstrap" {
1060+
for_each = local.bootstrap_tls_domains
1061+
private_key_pem = tls_private_key.bootstrap[each.key].private_key_pem
1062+
1063+
subject {
1064+
common_name = each.value.domain
1065+
}
1066+
1067+
validity_period_hours = 8760 # 1 year
1068+
1069+
dns_names = [
1070+
each.value.domain,
1071+
"*.${each.value.domain}"
1072+
]
1073+
1074+
allowed_uses = [
1075+
"key_encipherment",
1076+
"digital_signature",
1077+
"server_auth"
1078+
]
1079+
}
1080+
1081+
resource "kubernetes_secret_v1" "bootstrap_tls" {
1082+
for_each = local.bootstrap_tls_domains
1083+
1084+
metadata {
1085+
name = "${local.name}-${each.key}-tls-cert"
1086+
namespace = var.environment.namespace
1087+
}
1088+
1089+
data = {
1090+
"tls.crt" = tls_self_signed_cert.bootstrap[each.key].cert_pem
1091+
"tls.key" = tls_private_key.bootstrap[each.key].private_key_pem
1092+
}
1093+
1094+
type = "kubernetes.io/tls"
1095+
1096+
lifecycle {
1097+
ignore_changes = [data, metadata[0].annotations, metadata[0].labels]
1098+
}
1099+
}
1100+
1101+
# Bootstrap TLS for additional hostnames (from domain_prefix in rules)
1102+
# Only for additional hostnames that did NOT inherit a certificate_reference from parent domain
1103+
resource "tls_private_key" "bootstrap_additional" {
1104+
for_each = local.additional_hostname_configs
1105+
algorithm = "RSA"
1106+
rsa_bits = 2048
1107+
}
1108+
1109+
resource "tls_self_signed_cert" "bootstrap_additional" {
1110+
for_each = local.additional_hostname_configs
1111+
private_key_pem = tls_private_key.bootstrap_additional[each.key].private_key_pem
1112+
1113+
subject {
1114+
common_name = each.value.hostname
1115+
}
1116+
1117+
validity_period_hours = 8760 # 1 year
1118+
1119+
dns_names = [each.value.hostname]
1120+
1121+
allowed_uses = [
1122+
"key_encipherment",
1123+
"digital_signature",
1124+
"server_auth"
1125+
]
1126+
}
1127+
1128+
resource "kubernetes_secret_v1" "bootstrap_tls_additional" {
1129+
for_each = local.additional_hostname_configs
1130+
1131+
metadata {
1132+
name = each.value.secret_name
1133+
namespace = var.environment.namespace
1134+
}
1135+
1136+
data = {
1137+
"tls.crt" = tls_self_signed_cert.bootstrap_additional[each.key].cert_pem
1138+
"tls.key" = tls_private_key.bootstrap_additional[each.key].private_key_pem
1139+
}
1140+
1141+
type = "kubernetes.io/tls"
1142+
1143+
lifecycle {
1144+
ignore_changes = [data, metadata[0].annotations, metadata[0].labels]
1145+
}
1146+
}
1147+
10401148
# Helm release name - keep under 34 chars so that fullname (release + "-" + chart name = 34+1+20 = 55)
10411149
# plus the longest suffix (-certgen, 8 chars) stays within the 63-char k8s label value limit.
10421150
#
@@ -1274,6 +1382,11 @@ resource "helm_release" "nginx_gateway_fabric" {
12741382
}),
12751383
yamlencode(local.base_helm_values)
12761384
]
1385+
1386+
depends_on = [
1387+
kubernetes_secret_v1.bootstrap_tls,
1388+
kubernetes_secret_v1.bootstrap_tls_additional
1389+
]
12771390
}
12781391

12791392
# Deploy all Gateway API resources using facets-utility-modules

0 commit comments

Comments
 (0)