Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- namespaces
- pods
Expand Down
1 change: 1 addition & 0 deletions docs/en/latest/concepts/gateway-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,5 @@ The fields below are specified in the Gateway API specification but are either p
| `spec.listeners[].tls.certificateRefs[].group` | Partially supported | Only `""` is supported; other group values cause validation failure. |
| `spec.listeners[].tls.certificateRefs[].kind` | Partially supported | Only `Secret` is supported. |
| `spec.listeners[].tls.mode` | Partially supported | `Terminate` is implemented; `Passthrough` is effectively unsupported for Gateway listeners. |
| `spec.listeners[].tls.frontendValidation` | Partially supported | Enables downstream (client) mTLS. `caCertificateRefs` may reference a `ConfigMap` (Gateway API Core support) or a `Secret` (implementation-specific) holding the CA certificate under the `ca.crt` key; clients are then required to present a certificate signed by one of the referenced CAs. |
| `spec.addresses` | Not supported | Controller does not read or act on `spec.addresses`. |
68 changes: 68 additions & 0 deletions internal/adc/translator/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ package translator
import (
"encoding/json"
"fmt"
"strings"

"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

Expand Down Expand Up @@ -76,6 +78,12 @@ func (t *Translator) translateSecret(tctx *provider.TranslateContext, listener g
sslObjs := make([]*adctypes.SSL, 0)
switch *listener.TLS.Mode {
case gatewayv1.TLSModeTerminate:
// frontendValidation configures downstream mTLS: clients must present a
// certificate signed by one of the referenced CAs during the TLS handshake.
client, err := t.translateFrontendValidation(tctx, listener, obj)
if err != nil {
return nil, err
}
for refIndex, ref := range listener.TLS.CertificateRefs {
ns := obj.GetNamespace()
if ref.Namespace != nil {
Expand Down Expand Up @@ -118,6 +126,7 @@ func (t *Translator) translateSecret(tctx *provider.TranslateContext, listener g
}
sslObj.Snis = append(sslObj.Snis, hosts...)
}
sslObj.Client = client
sslObj.ID = id.GenID(fmt.Sprintf("%s_%s_%d", adctypes.ComposeSSLName(internaltypes.KindGateway, obj.Namespace, obj.Name), listener.Name, refIndex))
t.Log.V(1).Info("generated ssl id", "ssl id", sslObj.ID, "secret", secretNN.String())
sslObj.Labels = label.GenLabel(obj)
Expand All @@ -135,6 +144,65 @@ func (t *Translator) translateSecret(tctx *provider.TranslateContext, listener g
return sslObjs, nil
}

// translateFrontendValidation builds the downstream mTLS client configuration from a
// listener's frontendValidation. The referenced CA certificates (ConfigMap, key `ca.crt`)
// are bundled into a single trust anchor used to validate client certificates.
func (t *Translator) translateFrontendValidation(tctx *provider.TranslateContext, listener gatewayv1.Listener, obj *gatewayv1.Gateway) (*adctypes.ClientClass, error) {
if listener.TLS.FrontendValidation == nil || len(listener.TLS.FrontendValidation.CACertificateRefs) == 0 {
return nil, nil
}

cas := make([]string, 0, len(listener.TLS.FrontendValidation.CACertificateRefs))
for _, ref := range listener.TLS.FrontendValidation.CACertificateRefs {
// caCertificateRefs must be in the core API group. ConfigMap is the
// Gateway API Core support; Secret is an implementation-specific extension.
if ref.Group != "" && string(ref.Group) != corev1.GroupName {
return nil, fmt.Errorf("unsupported frontendValidation caCertificateRef group %q in listener %s, only the core group is supported", ref.Group, listener.Name)
}
ns := obj.GetNamespace()
if ref.Namespace != nil {
ns = string(*ref.Namespace)
}
nn := types.NamespacedName{Namespace: ns, Name: string(ref.Name)}

kind := internaltypes.KindConfigMap
if ref.Kind != "" {
kind = string(ref.Kind)
}
var (
ca []byte
err error
)
switch kind {
case internaltypes.KindConfigMap:
cm := tctx.ConfigMaps[nn]
if cm == nil {
return nil, fmt.Errorf("frontendValidation CA ConfigMap %s not found", nn.String())
}
if ca, err = sslutils.ExtractCAFromConfigMap(cm); err != nil {
t.Log.Error(err, "failed to extract CA from configmap", "configmap", nn.String())
return nil, fmt.Errorf("failed to extract CA from ConfigMap %s: %w", nn.String(), err)
}
case internaltypes.KindSecret:
secret := tctx.Secrets[nn]
if secret == nil {
return nil, fmt.Errorf("frontendValidation CA Secret %s not found", nn.String())
}
if ca, err = sslutils.ExtractCAFromSecret(secret); err != nil {
t.Log.Error(err, "failed to extract CA from secret", "secret", nn.String())
return nil, fmt.Errorf("failed to extract CA from Secret %s: %w", nn.String(), err)
}
default:
return nil, fmt.Errorf("unsupported frontendValidation caCertificateRef kind %q in listener %s, only ConfigMap and Secret are supported", ref.Kind, listener.Name)
}
cas = append(cas, strings.TrimSpace(string(ca)))
}

return &adctypes.ClientClass{
CA: strings.Join(cas, "\n"),
}, nil
}

// fillPluginsFromGatewayProxy fill plugins from GatewayProxy to given plugins
func (t *Translator) fillPluginsFromGatewayProxy(plugins adctypes.GlobalRule, gatewayProxy *v1alpha1.GatewayProxy) {
if gatewayProxy == nil {
Expand Down
210 changes: 210 additions & 0 deletions internal/adc/translator/gateway_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package translator

import (
"context"
"testing"

"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

"github.com/apache/apisix-ingress-controller/internal/provider"
)

const testCACert = `-----BEGIN CERTIFICATE-----
MIIBQzCB6qADAgECAgEBMAoGCCqGSM49BAMCMBIxEDAOBgNVBAMTB3Rlc3QtY2Ew
HhcNNzAwMTAxMDAwMDAwWhcNMzgwMTE5MDMxNDA4WjASMRAwDgYDVQQDEwd0ZXN0
LWNhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJo4AsM30ZHN+mYeHjqwceGBz
V2bMz1+OyNXuaPYVrSF7HShZhanOYNHb6QLNhjGxMsBDQHVLolPjyTQJp9R5GqMx
MC8wDgYDVR0PAQH/BAQDAgIEMB0GA1UdDgQWBBRzjh0YVmnpN/cFJziO0aYySuti
4DAKBggqhkjOPQQDAgNIADBFAiEA7fEGiQA7wX0LrrkRH4KplAPOgVV5Kvm/1dv1
3TLq9ssCIHKkv2dhydRvv36KC1WsRDcrl7W+7YmEnCS9PZfb8agM
-----END CERTIFICATE-----`

func newTLSGateway(frontendValidation *gatewayv1.FrontendTLSValidation) *gatewayv1.Gateway {
return &gatewayv1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "gw",
},
Spec: gatewayv1.GatewaySpec{
Listeners: []gatewayv1.Listener{
{
Name: "https",
Hostname: ptr.To(gatewayv1.Hostname("example.com")),
TLS: &gatewayv1.GatewayTLSConfig{
Mode: ptr.To(gatewayv1.TLSModeTerminate),
CertificateRefs: []gatewayv1.SecretObjectReference{
{
Kind: ptr.To(gatewayv1.Kind("Secret")),
Name: gatewayv1.ObjectName("server-cert"),
},
},
FrontendValidation: frontendValidation,
},
},
},
},
}
}

func newTranslateContextWithTLS() *provider.TranslateContext {
tctx := provider.NewDefaultTranslateContext(context.Background())
tctx.Secrets[types.NamespacedName{Namespace: "default", Name: "server-cert"}] = &corev1.Secret{
Data: map[string][]byte{
"cert": []byte("server-cert-data"),
"key": []byte("server-key-data"),
},
}
tctx.ConfigMaps[types.NamespacedName{Namespace: "default", Name: "ca-cm"}] = &corev1.ConfigMap{
Data: map[string]string{
corev1.ServiceAccountRootCAKey: testCACert,
},
}
tctx.Secrets[types.NamespacedName{Namespace: "default", Name: "ca-secret"}] = &corev1.Secret{
Data: map[string][]byte{
corev1.ServiceAccountRootCAKey: []byte(testCACert),
},
}
return tctx
}

func TestTranslateSecret_FrontendValidation(t *testing.T) {
t.Run("with frontendValidation sets downstream mTLS client CA", func(t *testing.T) {
tr := &Translator{Log: logr.Discard()}
gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
CACertificateRefs: []gatewayv1.ObjectReference{
{
Group: "",
Kind: "ConfigMap",
Name: "ca-cm",
},
},
})
tctx := newTranslateContextWithTLS()

sslObjs, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], gateway)
require.NoError(t, err)
require.Len(t, sslObjs, 1)
require.NotNil(t, sslObjs[0].Client, "client mTLS config should be set")
assert.Equal(t, testCACert, sslObjs[0].Client.CA)
assert.Equal(t, []string{"example.com"}, sslObjs[0].Snis)
})

t.Run("with Secret CA ref sets downstream mTLS client CA", func(t *testing.T) {
tr := &Translator{Log: logr.Discard()}
gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
CACertificateRefs: []gatewayv1.ObjectReference{
{Group: "", Kind: "Secret", Name: "ca-secret"},
},
})
tctx := newTranslateContextWithTLS()

sslObjs, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], gateway)
require.NoError(t, err)
require.Len(t, sslObjs, 1)
require.NotNil(t, sslObjs[0].Client, "client mTLS config should be set")
assert.Equal(t, testCACert, sslObjs[0].Client.CA)
})

t.Run("missing CA Secret returns error", func(t *testing.T) {
tr := &Translator{Log: logr.Discard()}
gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
CACertificateRefs: []gatewayv1.ObjectReference{
{Kind: "Secret", Name: "missing"},
},
})
tctx := newTranslateContextWithTLS()

_, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], gateway)
require.Error(t, err)
})

t.Run("without frontendValidation leaves client nil", func(t *testing.T) {
tr := &Translator{Log: logr.Discard()}
gateway := newTLSGateway(nil)
tctx := newTranslateContextWithTLS()

sslObjs, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], gateway)
require.NoError(t, err)
require.Len(t, sslObjs, 1)
assert.Nil(t, sslObjs[0].Client)
})

t.Run("missing CA ConfigMap returns error", func(t *testing.T) {
tr := &Translator{Log: logr.Discard()}
gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
CACertificateRefs: []gatewayv1.ObjectReference{
{Kind: "ConfigMap", Name: "missing"},
},
})
tctx := newTranslateContextWithTLS()

_, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], gateway)
require.Error(t, err)
})

t.Run("unsupported CA ref kind returns error", func(t *testing.T) {
tr := &Translator{Log: logr.Discard()}
gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
CACertificateRefs: []gatewayv1.ObjectReference{
{Kind: "Pod", Name: "ca-cm"},
},
})
tctx := newTranslateContextWithTLS()

_, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], gateway)
require.Error(t, err)
})

t.Run("unsupported CA ref group returns error", func(t *testing.T) {
tr := &Translator{Log: logr.Discard()}
gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
CACertificateRefs: []gatewayv1.ObjectReference{
{Group: "example.com", Kind: "ConfigMap", Name: "ca-cm"},
},
})
tctx := newTranslateContextWithTLS()

_, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], gateway)
require.Error(t, err)
})

t.Run("malformed CA data returns error", func(t *testing.T) {
tr := &Translator{Log: logr.Discard()}
gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
CACertificateRefs: []gatewayv1.ObjectReference{
{Kind: "ConfigMap", Name: "ca-cm"},
},
})
tctx := newTranslateContextWithTLS()
tctx.ConfigMaps[types.NamespacedName{Namespace: "default", Name: "ca-cm"}] = &corev1.ConfigMap{
Data: map[string]string{corev1.ServiceAccountRootCAKey: " not a pem cert "},
}

_, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], gateway)
require.Error(t, err)
})
}
Loading
Loading