Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
82fd817
Support FrontentTLS through secrets.
shaun-nx Apr 2, 2026
1fc56a2
Ensure SSL servers use the right cert file
shaun-nx Apr 7, 2026
d657fd0
Support ConfigMaps as a caCertificateRef
shaun-nx Apr 10, 2026
886a0b3
Configure ssl_verify_client
shaun-nx Apr 10, 2026
4981f75
Handle NoValidCACertificate listener reason
shaun-nx Apr 10, 2026
6dc2050
Add comments for new listener conditions
shaun-nx Apr 13, 2026
cf61169
Check for TLS mode
shaun-nx Apr 13, 2026
43bcbe2
Add comments and cleanup code
shaun-nx Apr 13, 2026
9335bbe
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 13, 2026
92d1312
Unite tests for servers.go and conditions.go
shaun-nx Apr 13, 2026
749353b
Move FrontendTLSConfig from Gateway to Listener
shaun-nx Apr 13, 2026
2eec5c1
Move function from gateway to listener
shaun-nx Apr 13, 2026
9b6da26
Fix failing unit tests
shaun-nx Apr 13, 2026
7cfdb55
Pass Conformance tests
shaun-nx Apr 13, 2026
5ebf087
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 14, 2026
b1a6a5d
Code cleanup
shaun-nx Apr 14, 2026
b8f8b52
Unit tests for gateway_listener
shaun-nx Apr 14, 2026
f1fa886
Simplify buildFrontendTLSCertBundles. Add Unit tests
shaun-nx Apr 14, 2026
343d045
Fix validation mode logic
shaun-nx Apr 14, 2026
c3eb9f1
Fix template indentation
shaun-nx Apr 14, 2026
f1f87aa
Unit tests for referencegrant, configmaps and secrets
shaun-nx Apr 14, 2026
fab7e55
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 14, 2026
5bac074
Add FrontenedTLS validation to supported features. Code review updates.
shaun-nx Apr 16, 2026
f67340e
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 16, 2026
91b188c
Cleanup logic for cert bundle generation
shaun-nx Apr 17, 2026
d628721
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 17, 2026
7d86a2b
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 20, 2026
3213c05
Do not create bundle when mode is AllowInsecureFallback
shaun-nx Apr 20, 2026
1c89767
Update secrets revalidate logic. Add tests for condition setting in p…
shaun-nx Apr 22, 2026
c76a76d
Check CaCertRef namespace against refCertBundles
shaun-nx Apr 22, 2026
8b29f8d
Add ExecuteServer test for FrontendTLS
shaun-nx Apr 22, 2026
d9d6a5c
Remove test comments
shaun-nx Apr 22, 2026
8e7de84
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 22, 2026
b88b883
Add client verification to default SSL servers
shaun-nx Apr 22, 2026
36cdd30
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 22, 2026
2a4b6cb
Fix if statment in template
shaun-nx Apr 22, 2026
1091586
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 22, 2026
2152f2e
Create map from refCertBundles
shaun-nx Apr 22, 2026
4ab7479
Update bundle ref ID to incldue gateway namespace and name
shaun-nx Apr 22, 2026
a88b00f
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 22, 2026
84aaf37
Fix unit test
shaun-nx Apr 22, 2026
6d0847f
Validate ca.crt data for Opaque secrets. Additional code review changes
shaun-nx Apr 23, 2026
97521de
Fix if check in validateOpaqueSecretKey
shaun-nx Apr 23, 2026
dfba628
Update validation mode condition message
shaun-nx Apr 23, 2026
f817ec6
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 23, 2026
15ccdec
Remove duplicate ca ref processing
shaun-nx Apr 24, 2026
a26cfcf
Improve ssl server client setting assignment
shaun-nx Apr 24, 2026
4d46921
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 27, 2026
72d6bfa
Re-arrange server template
shaun-nx Apr 27, 2026
059917f
Update comments. Add index to PerPort field path
shaun-nx Apr 27, 2026
84470ca
Update internal/controller/nginx/config/servers_test.go
shaun-nx Apr 27, 2026
84c7351
Update internal/controller/state/resolver/secrets.go
shaun-nx Apr 27, 2026
6177eed
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 27, 2026
3b1779b
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 27, 2026
1e3137c
Merge branch 'main' into feat/frontendtls
shaun-nx Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/controller/nginx/config/http/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,11 @@ type Return struct {
type SSL struct {
Protocols string
Ciphers string
ClientCertificate string
VerifyClient string
Certificates []string
CertificateKeys []string
RequireVerifiedCert bool
PreferServerCiphers bool
}

Expand Down
14 changes: 14 additions & 0 deletions internal/controller/nginx/config/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,18 +222,32 @@ func buildHTTPSSL(ssl *dataplane.SSL) *http.SSL {
certs := make([]string, 0, len(ssl.KeyPairIDs))
keys := make([]string, 0, len(ssl.KeyPairIDs))

var sslCertificateID string
var sslVerifyClient string

for _, id := range ssl.KeyPairIDs {
pemFile := generatePEMFileName(id)
certs = append(certs, pemFile)
keys = append(keys, pemFile)
}

if ssl.ClientCertBundleID != "" {
sslCertificateID = generateCertBundleFileName(ssl.ClientCertBundleID)
}

if ssl.VerifyClient != "" {
sslVerifyClient = ssl.VerifyClient
}
Comment thread
shaun-nx marked this conversation as resolved.
Outdated

return &http.SSL{
Certificates: certs,
CertificateKeys: keys,
Protocols: ssl.Protocols,
Ciphers: ssl.Ciphers,
PreferServerCiphers: ssl.PreferServerCiphers,
ClientCertificate: sslCertificateID,
VerifyClient: sslVerifyClient,
RequireVerifiedCert: ssl.RequireVerifiedCert,
}
}

Expand Down
18 changes: 18 additions & 0 deletions internal/controller/nginx/config/servers_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ server {
{{- if $s.SSL.PreferServerCiphers }}
ssl_prefer_server_ciphers on;
{{- end }}
{{- if $s.SSL.ClientCertificate }}
ssl_client_certificate {{ $s.SSL.ClientCertificate }};
{{- end }}
{{- if $s.SSL.VerifyClient }}
ssl_verify_client {{ $s.SSL.VerifyClient }};
{{- end }}
{{- if $s.SSL.RequireVerifiedCert }}
error_page 495 496 = @frontend_tls_verify_failed;
Comment thread
sjberman marked this conversation as resolved.
if ($ssl_client_verify != SUCCESS) {
return 444;
Comment thread
shaun-nx marked this conversation as resolved.
}
{{- end }}
Comment thread
shaun-nx marked this conversation as resolved.

{{- if $s.MisdirectedRequestVars }}
if ({{ $s.MisdirectedRequestVars.SNIVar }} != {{ $s.MisdirectedRequestVars.HostVar }}) {
Expand Down Expand Up @@ -244,6 +256,12 @@ server {
}
{{- end }}

{{- if and $s.SSL $s.SSL.RequireVerifiedCert }}
location @frontend_tls_verify_failed {
return 444;
}
{{- end }}

{{- if $s.GRPC }}
include /etc/nginx/grpc-error-locations.conf;
{{- end }}
Expand Down
49 changes: 49 additions & 0 deletions internal/controller/nginx/config/servers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6783,3 +6783,52 @@ func TestExistingExactPathSet(t *testing.T) {
})
}
}

func TestBuildHTTPSSLFrontendTLS(t *testing.T) {
t.Parallel()

tests := []struct {
name string
ssl *dataplane.SSL
expectedClientCertificate string
expectedVerifyClient string
expectedRequireVerified bool
}{
{
name: "frontend TLS disabled by default",
ssl: &dataplane.SSL{
KeyPairIDs: []dataplane.SSLKeyPairID{"test-keypair"},
},
expectedClientCertificate: "",
expectedVerifyClient: "",
expectedRequireVerified: false,
},
{
name: "frontend TLS client certificate verification enabled",
ssl: &dataplane.SSL{
KeyPairIDs: []dataplane.SSLKeyPairID{"test-keypair"},
ClientCertBundleID: dataplane.CertBundleID("test-ca-bundle"),
VerifyClient: "optional_no_ca",
RequireVerifiedCert: true,
},
expectedClientCertificate: generateCertBundleFileName(dataplane.CertBundleID("test-ca-bundle")),
expectedVerifyClient: "optional_no_ca",
Comment thread
shaun-nx marked this conversation as resolved.
Outdated
expectedRequireVerified: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
g := NewWithT(t)

result := buildHTTPSSL(test.ssl)

g.Expect(result.ClientCertificate).To(Equal(test.expectedClientCertificate))
g.Expect(result.VerifyClient).To(Equal(test.expectedVerifyClient))
g.Expect(result.RequireVerifiedCert).To(Equal(test.expectedRequireVerified))
g.Expect(result.Certificates).To(Equal([]string{generatePEMFileName(dataplane.SSLKeyPairID("test-keypair"))}))
g.Expect(result.CertificateKeys).To(Equal([]string{generatePEMFileName(dataplane.SSLKeyPairID("test-keypair"))}))
})
}
}
Comment thread
shaun-nx marked this conversation as resolved.
68 changes: 65 additions & 3 deletions internal/controller/state/conditions/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,10 +571,14 @@ func NewRouteResolvedRefsInvalidFilter(msg string) Condition {
// the NoConflicts condition is excluded to avoid conflicting condition states.
func NewDefaultListenerConditions(existingConditions []Condition) []Condition {
defaultConds := []Condition{
NewListenerAccepted(),
NewListenerProgrammed(),
}

// Only add Accepted=true if there are no existing Accepted=false conditions.
if !hasNotAcceptedListener(existingConditions) {
defaultConds = append(defaultConds, NewListenerAccepted())
}

Comment thread
shaun-nx marked this conversation as resolved.
Outdated
// Only add ResolvedRefs=true if there are no existing ResolvedRefs conditions
if !hasResolvedRefsConditions(existingConditions) {
defaultConds = append(defaultConds, NewListenerResolvedRefs())
Expand All @@ -588,10 +592,20 @@ func NewDefaultListenerConditions(existingConditions []Condition) []Condition {
return defaultConds
}

// hasResolvedRefsConditions checks if there are any existing ResolvedRefs conditions.
// hasNotAcceptedListener checks if the Listener has an Accepted=False condition.
func hasNotAcceptedListener(conditions []Condition) bool {
for _, cond := range conditions {
if cond.Type == string(v1.ListenerConditionAccepted) && cond.Status == metav1.ConditionFalse {
Comment thread
shaun-nx marked this conversation as resolved.
Outdated
return true
}
}
return false
}

// hasResolvedRefsConditions checks if the Listener has any ResolvedRefs=False conditions.
func hasResolvedRefsConditions(conditions []Condition) bool {
for _, cond := range conditions {
if cond.Type == string(v1.ListenerConditionResolvedRefs) {
if cond.Type == string(v1.ListenerConditionResolvedRefs) && cond.Status == metav1.ConditionFalse {
Comment thread
shaun-nx marked this conversation as resolved.
Outdated
return true
}
}
Expand Down Expand Up @@ -1005,6 +1019,54 @@ func NewGatewayNotProgrammedInvalid(msg string) Condition {
}
}

// NewGatewayInsecureFrontendValidationMode returns a Condition that indicates
// the Gateway is accepted, but is using an insecure frontend validation mode.
Comment thread
shaun-nx marked this conversation as resolved.
func NewGatewayInsecureFrontendValidationMode(msg string) Condition {
return Condition{
Type: string(v1.GatewayConditionInsecureFrontendValidationMode),
Status: metav1.ConditionTrue,
Reason: string(v1.GatewayReasonConfigurationChanged),
Message: msg,
}
}

// NewListenerInvalidCaCertificateRef returns a Condition indicating
// that a CA CertificateRef for a Listener is invalid.
func NewListenerInvalidCaCertificateRef(msg string) Condition {
return Condition{
Type: string(v1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
Reason: string(v1.ListenerReasonInvalidCACertificateRef),
Message: msg,
}
}

// NewListenerInvalidCaCertificateKind returns a Condition indicating
// that a CA CertificateRef Kind for a Listener is invalid.
func NewListenerInvalidCaCertificateKind(msg string) Condition {
return Condition{
Type: string(v1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
Reason: string(v1.ListenerReasonInvalidCACertificateKind),
Message: msg,
}
}

// NewListenerInvalidNoValidCACertificate returns Conditions indicating
// that all CA Certificates for a Listener are invalid.
// It marks the listener as not Accepted and not Programmed.
func NewListenerInvalidNoValidCACertificate(msg string) []Condition {
return []Condition{
{
Type: string(v1.ListenerConditionAccepted),
Status: metav1.ConditionFalse,
Reason: string(v1.ListenerReasonNoValidCACertificate),
Message: msg,
},
NewListenerNotProgrammedInvalid(msg),
}
}

// NewNginxGatewayValid returns a Condition that indicates that the NginxGateway config is valid.
func NewNginxGatewayValid() Condition {
return Condition{
Expand Down
105 changes: 101 additions & 4 deletions internal/controller/state/conditions/conditions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,26 +153,74 @@ func TestNewDefaultListenerConditions(t *testing.T) {
tests := []struct {
name string
existingConditions []Condition
expectAccepted bool
expectResolvedRefs bool
expectNoConflicts bool
}{
{
name: "no existing conditions includes ResolvedRefs",
name: "no existing conditions includes all defaults",
existingConditions: nil,
expectAccepted: true,
expectResolvedRefs: true,
expectNoConflicts: true,
},
{
name: "existing ResolvedRefs condition (InvalidCertificateRef) is preserved",
name: "existing ResolvedRefs=False (InvalidCertificateRef) suppresses default ResolvedRefs",
existingConditions: []Condition{
NewListenerUnresolvedCertificateRef("some cert ref error", string(v1.ListenerReasonInvalidCertificateRef)),
},
expectAccepted: true,
expectResolvedRefs: false,
expectNoConflicts: true,
},
{
name: "existing ResolvedRefs condition (RefNotPermitted) is preserved",
name: "existing ResolvedRefs=False (RefNotPermitted) suppresses default ResolvedRefs",
existingConditions: []Condition{
NewListenerUnresolvedCertificateRef("some ref not permitted error", string(v1.ListenerReasonRefNotPermitted)),
},
expectAccepted: true,
expectResolvedRefs: false,
expectNoConflicts: true,
},
{
name: "existing Accepted=False suppresses default Accepted",
existingConditions: NewListenerInvalidNoValidCACertificate("no valid CA certificate"),
expectAccepted: false,
expectResolvedRefs: true,
expectNoConflicts: true,
},
{
name: "existing Conflicted condition suppresses default NoConflicts",
existingConditions: []Condition{
{
Type: string(v1.ListenerConditionConflicted),
Status: metav1.ConditionTrue,
Reason: "SomeConflict",
},
},
expectAccepted: true,
expectResolvedRefs: true,
expectNoConflicts: false,
},
{
name: "existing OverlappingTLSConfig condition suppresses default NoConflicts",
existingConditions: []Condition{
NewListenerOverlappingTLSConfig(v1.ListenerReasonHostnameConflict, "overlapping TLS"),
},
expectAccepted: true,
expectResolvedRefs: true,
expectNoConflicts: false,
},
{
name: "multiple existing failure conditions suppress corresponding defaults",
existingConditions: append(
NewListenerInvalidNoValidCACertificate("no valid CA certificate"),
NewListenerInvalidCaCertificateRef("invalid CA cert ref"),
NewListenerOverlappingTLSConfig(v1.ListenerReasonHostnameConflict, "overlapping TLS"),
),
expectAccepted: false,
expectResolvedRefs: false,
expectNoConflicts: false,
},
}

Expand All @@ -183,13 +231,62 @@ func TestNewDefaultListenerConditions(t *testing.T) {

result := NewDefaultListenerConditions(test.existingConditions)

hasAccepted := false
hasResolvedRefs := false
hasNoConflicts := false
for _, c := range result {
if c.Type == string(v1.ListenerConditionResolvedRefs) {
switch c.Type {
case string(v1.ListenerConditionAccepted):
hasAccepted = true
case string(v1.ListenerConditionResolvedRefs):
hasResolvedRefs = true
case string(v1.ListenerConditionConflicted):
hasNoConflicts = true
}
}
g.Expect(hasAccepted).To(Equal(test.expectAccepted))
g.Expect(hasResolvedRefs).To(Equal(test.expectResolvedRefs))
g.Expect(hasNoConflicts).To(Equal(test.expectNoConflicts))
})
}
}

func TestNewListenerCACertificateConditions(t *testing.T) {
Comment thread
shaun-nx marked this conversation as resolved.
t.Parallel()

t.Run("NewListenerInvalidCaCertificateRef", func(t *testing.T) {
t.Parallel()
g := NewWithT(t)

cond := NewListenerInvalidCaCertificateRef("invalid CA cert ref")
g.Expect(cond.Type).To(Equal(string(v1.ListenerConditionResolvedRefs)))
g.Expect(cond.Status).To(Equal(metav1.ConditionFalse))
g.Expect(cond.Reason).To(Equal(string(v1.ListenerReasonInvalidCACertificateRef)))
g.Expect(cond.Message).To(Equal("invalid CA cert ref"))
})

t.Run("NewListenerInvalidCaCertificateKind", func(t *testing.T) {
t.Parallel()
g := NewWithT(t)

cond := NewListenerInvalidCaCertificateKind("invalid CA cert kind")
g.Expect(cond.Type).To(Equal(string(v1.ListenerConditionResolvedRefs)))
g.Expect(cond.Status).To(Equal(metav1.ConditionFalse))
g.Expect(cond.Reason).To(Equal(string(v1.ListenerReasonInvalidCACertificateKind)))
g.Expect(cond.Message).To(Equal("invalid CA cert kind"))
})

t.Run("NewListenerInvalidNoValidCACertificate", func(t *testing.T) {
t.Parallel()
g := NewWithT(t)

conds := NewListenerInvalidNoValidCACertificate("all CA certs invalid")
g.Expect(conds).To(HaveLen(2))
g.Expect(conds[0].Type).To(Equal(string(v1.ListenerConditionAccepted)))
g.Expect(conds[0].Status).To(Equal(metav1.ConditionFalse))
g.Expect(conds[0].Reason).To(Equal(string(v1.ListenerReasonNoValidCACertificate)))
g.Expect(conds[0].Message).To(Equal("all CA certs invalid"))
g.Expect(conds[1].Type).To(Equal(string(v1.ListenerConditionProgrammed)))
g.Expect(conds[1].Status).To(Equal(metav1.ConditionFalse))
})
}
Loading
Loading