Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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` must reference a `ConfigMap` 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`. |
44 changes: 44 additions & 0 deletions internal/adc/translator/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package translator
import (
"encoding/json"
"fmt"
"strings"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -76,6 +77,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 +125,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 +143,42 @@ 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 {
// Core support is limited to ConfigMap references.
if ref.Kind != "" && string(ref.Kind) != internaltypes.KindConfigMap {
return nil, fmt.Errorf("unsupported frontendValidation caCertificateRef kind %q in listener %s, only ConfigMap is supported", ref.Kind, listener.Name)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
ns := obj.GetNamespace()
if ref.Namespace != nil {
ns = string(*ref.Namespace)
}
cmNN := types.NamespacedName{Namespace: ns, Name: string(ref.Name)}
cm := tctx.ConfigMaps[cmNN]
if cm == nil {
return nil, fmt.Errorf("frontendValidation CA ConfigMap %s not found", cmNN.String())
}
ca, err := sslutils.ExtractCAFromConfigMap(cm)
if err != nil {
t.Log.Error(err, "failed to extract CA from configmap", "configmap", cmNN.String())
return nil, fmt.Errorf("failed to extract CA from ConfigMap %s: %w", cmNN.String(), err)
}
cas = append(cas, strings.TrimSpace(string(ca)))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

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
139 changes: 139 additions & 0 deletions internal/adc/translator/gateway_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// 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-----\nMIIBCA-test-ca\n-----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,
},
}
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("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: "Secret", Name: "ca-cm"},
},
})
tctx := newTranslateContextWithTLS()

_, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], gateway)
require.Error(t, err)
})
}
58 changes: 57 additions & 1 deletion internal/controller/gateway_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ func (r *GatewayReconciler) SetupWithManager(mgr ctrl.Manager) error {
Watches(
&corev1.Secret{},
handler.EnqueueRequestsFromMapFunc(r.listGatewaysForSecret),
).
Watches(
&corev1.ConfigMap{},
handler.EnqueueRequestsFromMapFunc(r.listGatewaysForConfigMap),
)

if GetEnableReferenceGrant() {
Expand Down Expand Up @@ -390,6 +394,34 @@ func (r *GatewayReconciler) listGatewaysForSecret(ctx context.Context, obj clien
return requests
}

func (r *GatewayReconciler) listGatewaysForConfigMap(ctx context.Context, obj client.Object) (requests []reconcile.Request) {
configMap, ok := obj.(*corev1.ConfigMap)
if !ok {
r.Log.Error(
errors.New("unexpected object type"),
"ConfigMap watch predicate received unexpected object type",
"expected", FullTypeName(new(corev1.ConfigMap)), "found", FullTypeName(obj),
)
return nil
}
var gatewayList gatewayv1.GatewayList
if err := r.List(ctx, &gatewayList, client.MatchingFields{
indexer.ConfigMapIndexRef: indexer.GenIndexKey(configMap.GetNamespace(), configMap.GetName()),
}); err != nil {
r.Log.Error(err, "failed to list gateways for configmap")
return nil
}
for _, gateway := range gatewayList.Items {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: gateway.GetNamespace(),
Name: gateway.GetName(),
},
})
}
return requests
}

func (r *GatewayReconciler) listReferenceGrantsForGateway(ctx context.Context, obj client.Object) (requests []reconcile.Request) {
grant, ok := obj.(*v1beta1.ReferenceGrant)
if !ok {
Expand Down Expand Up @@ -434,7 +466,7 @@ func (r *GatewayReconciler) processInfrastructure(tctx *provider.TranslateContex
func (r *GatewayReconciler) processListenerConfig(tctx *provider.TranslateContext, gateway *gatewayv1.Gateway) {
listeners := gateway.Spec.Listeners
for _, listener := range listeners {
if listener.TLS == nil || listener.TLS.CertificateRefs == nil {
if listener.TLS == nil {
continue
}
secret := corev1.Secret{}
Expand All @@ -457,5 +489,29 @@ func (r *GatewayReconciler) processListenerConfig(tctx *provider.TranslateContex
tctx.Secrets[types.NamespacedName{Namespace: ns, Name: string(ref.Name)}] = &secret
}
}
// frontendValidation references CA ConfigMaps used for downstream mTLS.
if listener.TLS.FrontendValidation != nil {
for _, ref := range listener.TLS.FrontendValidation.CACertificateRefs {
if ref.Kind != "" && string(ref.Kind) != KindConfigMap {
continue
}
ns := gateway.GetNamespace()
if ref.Namespace != nil {
ns = string(*ref.Namespace)
}
configMap := corev1.ConfigMap{}
if err := r.Get(context.Background(), client.ObjectKey{
Namespace: ns,
Name: string(ref.Name),
}, &configMap); err != nil {
r.Log.Error(err, "failed to get CA configmap", "namespace", ns, "name", ref.Name)
SetGatewayListenerConditionProgrammed(gateway, string(listener.Name), false, err.Error())
SetGatewayListenerConditionResolvedRefs(gateway, string(listener.Name), false, err.Error())
continue
}
r.Log.Info("Setting CA configmap for listener", "listener", listener.Name, "configmap", configMap.Name, "namespace", ns)
tctx.ConfigMaps[types.NamespacedName{Namespace: ns, Name: string(ref.Name)}] = &configMap
}
Comment thread
AlinsRan marked this conversation as resolved.
}
}
}
37 changes: 37 additions & 0 deletions internal/controller/indexer/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const (
ParentRefs = "parentRefs"
IngressClass = "ingressClass"
SecretIndexRef = "secretRefs"
ConfigMapIndexRef = "configMapRefs"
IngressClassRef = "ingressClassRef"
IngressClassParametersRef = "ingressClassParametersRef"
ConsumerGatewayRef = "consumerGatewayRef"
Expand Down Expand Up @@ -145,6 +146,15 @@ func setupGatewayIndexer(mgr ctrl.Manager) error {
return err
}

if err := mgr.GetFieldIndexer().IndexField(
context.Background(),
&gatewayv1.Gateway{},
ConfigMapIndexRef,
GatewayConfigMapIndexFunc,
); err != nil {
return err
}

if err := mgr.GetFieldIndexer().IndexField(
context.Background(),
&gatewayv1.Gateway{},
Expand Down Expand Up @@ -600,6 +610,33 @@ func GatewaySecretIndexFunc(rawObj client.Object) (keys []string) {
return keys
}

// GatewayConfigMapIndexFunc indexes Gateways by the CA ConfigMaps referenced via
// listener TLS frontendValidation, so that ConfigMap changes can trigger reconciliation.
func GatewayConfigMapIndexFunc(rawObj client.Object) (keys []string) {
gateway := rawObj.(*gatewayv1.Gateway)
var m = make(map[string]struct{})
for _, listener := range gateway.Spec.Listeners {
if listener.TLS == nil || listener.TLS.FrontendValidation == nil {
continue
}
for _, ref := range listener.TLS.FrontendValidation.CACertificateRefs {
if ref.Kind != "" && string(ref.Kind) != internaltypes.KindConfigMap {
continue
}
namespace := gateway.GetNamespace()
if ref.Namespace != nil {
namespace = string(*ref.Namespace)
}
key := GenIndexKey(namespace, string(ref.Name))
if _, ok := m[key]; !ok {
m[key] = struct{}{}
keys = append(keys, key)
}
}
}
return keys
}

func GenIndexKeyWithGK(group, kind, namespace, name string) string {
gvk := schema.GroupKind{
Group: group,
Expand Down
Loading
Loading