Skip to content

Commit

Permalink
Istio gateway: allow to specify namespace for TLS secret (#316)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinWeindel authored Oct 18, 2024
1 parent 00b2d08 commit 23ece9a
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 72 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ metadata:
#cert.gardener.cloud/preferred-chain: "chain name" # optional to specify preferred-chain (value is the Subject Common Name of the root issuer)
#cert.gardener.cloud/private-key-algorithm: ECDSA # optional to specify algorithm for private key, allowed values are 'RSA' or 'ECDSA'
#cert.gardener.cloud/private-key-size: "384" # optional to specify size of private key, allowed values for RSA are "2048", "3072", "4096" and for ECDSA "256" and "384"
#cert.gardener.cloud/secret-namespace: "my-namespace" # optional to specify the namespace where the certificate secret should be created
# annotations needed when using DNSRecords
#cert.gardener.cloud/dnsrecord-provider-type: aws-route53
#cert.gardener.cloud/dnsrecord-secret-ref: myns/mysecret
Expand Down Expand Up @@ -608,6 +609,9 @@ If you want to share a certificate between multiple services and ingresses, usin
This will create or reuse a certificate for `*.demo.mydomain.com`. An existing certificate is automatically reused,
if it has exactly the same common name and DNS names.

The annotation `cert.gardener.cloud/secret-namespace` can be used to change the namespace, the TLS secret is created in.
By default, it is created in the same namespace as the service.

## Demo quick start

1. Run dns-controller-manager with:
Expand Down Expand Up @@ -702,6 +706,7 @@ metadata:
#cert.gardener.cloud/preferred-chain: "chain name" # optional to specify preferred-chain (value is the Subject Common Name of the root issuer)
#cert.gardener.cloud/private-key-algorithm: ECDSA # optional to specify algorithm for private key, allowed values are 'RSA' or 'ECDSA'
#cert.gardener.cloud/private-key-size: "384" # optional to specify size of private key, allowed values for RSA are "2048", "3072", "4096" and for ECDSA "256" and "384"
#cert.gardener.cloud/secret-namespace: "istio-system" # optional to specify the namespace where the certificate secret should be created
# annotations needed when using DNSRecords
#cert.gardener.cloud/dnsrecord-provider-type: aws-route53
#cert.gardener.cloud/dnsrecord-secret-ref: myns/mysecret
Expand Down Expand Up @@ -736,6 +741,9 @@ spec:
In this case, only a `Certificate` resource would be created with domain name `*.example2.com`, as the first server item
specifies no `tls.credentialName` field.

The annotation `cert.gardener.cloud/secret-namespace` can be used to change the namespace, the TLS secret is created in.
By default, it is created in the same namespace as the gateway object.

See the [Istio tutorial](docs/usage/tutorials/istio-gateways.md) for a more detailed example.

### Gateway API gateways
Expand Down
3 changes: 2 additions & 1 deletion docs/usage/tutorials/istio-gateways.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ metadata:
name: httpbin-gateway
namespace: istio-system
annotations:
#cert.gardener.cloud/dnsnames: "*.example.com" # alternative if you want to control the dns names explicitly.
cert.gardener.cloud/purpose: managed
#cert.gardener.cloud/dnsnames: "*.example.com" # alternative if you want to control the dns names explicitly.
#cert.gardener.cloud/secret-namespace: "istio-system" # optional to specify the namespace where the certificate secret should be created
spec:
selector:
istio: ingressgateway # use Istio default gateway implementation
Expand Down
1 change: 1 addition & 0 deletions examples/40-gateway-istio.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ metadata:
#cert.gardener.cloud/preferred-chain: "chain name" # optional to specify preferred-chain (value is the Subject Common Name of the root issuer)
#cert.gardener.cloud/private-key-algorithm: ECDSA # optional to specify algorithm for private key, allowed values are 'RSA' or 'ECDSA'
#cert.gardener.cloud/private-key-size: "384" # optional to specify size of private key, allowed values for RSA are "2048", "3072", "4096" and for ECDSA "256" and "384"
#cert.gardener.cloud/secret-namespace: "istio-system" # optional to specify the namespace where the certificate secret should be created
name: my-gateway
namespace: default
spec:
Expand Down
2 changes: 2 additions & 0 deletions examples/40-service-loadbalancer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ metadata:
#cert.gardener.cloud/preferred-chain: "chain name" # optional to specify preferred-chain (value is the Subject Common Name of the root issuer)
#cert.gardener.cloud/private-key-algorithm: ECDSA # optional to specify algorithm for private key, allowed values are 'RSA' or 'ECDSA'
#cert.gardener.cloud/private-key-size: "384" # optional to specify size of private key, allowed values for RSA are "2048", "3072", "4096" and for ECDSA "256" and "384"
#cert.gardener.cloud/secret-namespace: "my-namespace" # optional to specify the namespace where the certificate secret should be created

# annotations needed when using DNSRecords
#cert.gardener.cloud/dnsrecord-provider-type: aws-route53
#cert.gardener.cloud/dnsrecord-secret-ref: myns/mysecret
Expand Down
2 changes: 2 additions & 0 deletions pkg/cert/source/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const (
AnnotForwardOwnerRefs = "cert.gardener.cloud/forward-owner-refs"
// AnnotSecretname is the annotation for the secret name
AnnotSecretname = "cert.gardener.cloud/secretname" // #nosec G101 -- this is no credential
// AnnotSecretNamespace is the annotation for the TLS secret namespace (only used for Istio Gateways source resources)
AnnotSecretNamespace = "cert.gardener.cloud/secret-namespace" // #nosec G101 -- this is no credential
// AnnotIssuer is the annotation for the issuer name
AnnotIssuer = "cert.gardener.cloud/issuer"
// AnnotCommonName is the annotation for explicitly specifying the common name
Expand Down
8 changes: 3 additions & 5 deletions pkg/cert/source/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"

"github.com/gardener/controller-manager-library/pkg/controllermanager/controller"
"github.com/gardener/controller-manager-library/pkg/controllermanager/controller/reconcile"
Expand Down Expand Up @@ -68,10 +69,7 @@ func (f *EventFeedback) Succeeded() {
func (f *EventFeedback) event(info *CertInfo, msg string, warning ...bool) {
channel := ""
if info != nil {
channel = info.SecretName
if info.SecretNamespace != nil {
channel = *info.SecretNamespace + "/" + info.SecretName
}
channel = info.SecretName.String()
}
if msg != f.events[channel] {
key := f.source.ClusterKey()
Expand Down Expand Up @@ -153,7 +151,7 @@ func (s *DefaultCertSource) GetEvents(key resources.ClusterObjectKey) map[string

// NewCertsInfo creates a CertsInfo
func NewCertsInfo() *CertsInfo {
return &CertsInfo{Certs: map[string]CertInfo{}}
return &CertsInfo{Certs: map[types.NamespacedName]CertInfo{}}
}

// CreateCertFeedback creates an event feedback for the given object.
Expand Down
14 changes: 7 additions & 7 deletions pkg/cert/source/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package source
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"

"github.com/gardener/controller-manager-library/pkg/controllermanager/controller"
"github.com/gardener/controller-manager-library/pkg/controllermanager/controller/reconcile"
Expand All @@ -20,8 +21,7 @@ import (

// CertInfo contains basic certificate data.
type CertInfo struct {
SecretNamespace *string
SecretName string
SecretName types.NamespacedName
Domains []string
IssuerName *string
FollowCNAME bool
Expand All @@ -34,7 +34,7 @@ type CertInfo struct {

// CertsInfo contains a map of CertInfo.
type CertsInfo struct {
Certs map[string]CertInfo
Certs map[types.NamespacedName]CertInfo
}

// CertFeedback is an interface for reporting certificate status.
Expand Down Expand Up @@ -65,7 +65,7 @@ type CertSourceType interface {
}

// CertTargetExtractor is type for extractor.
type CertTargetExtractor func(logger logger.LogContext, objData resources.ObjectData) (string, error)
type CertTargetExtractor func(logger logger.LogContext, objData resources.ObjectData) (types.NamespacedName, error)

// CertSourceCreator is type for creator.
type CertSourceCreator func(controller.Interface) (CertSource, error)
Expand All @@ -84,11 +84,11 @@ type CertState struct {

// CertCurrentState contains the current state.
type CertCurrentState struct {
CertStates map[string]*CertState
CertStates map[types.NamespacedName]*CertState
}

// ContainsSecretName returns true if name is in map.
func (s *CertCurrentState) ContainsSecretName(name string) bool {
// ContainsSecretName returns true if secret name is in map.
func (s *CertCurrentState) ContainsSecretName(name types.NamespacedName) bool {
_, ok := s.CertStates[name]
return ok
}
Expand Down
39 changes: 20 additions & 19 deletions pkg/cert/source/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (

core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/utils/ptr"

"github.com/gardener/controller-manager-library/pkg/controllermanager/controller"
Expand All @@ -22,14 +24,13 @@ import (
"github.com/gardener/controller-manager-library/pkg/logger"
"github.com/gardener/controller-manager-library/pkg/resources"
"github.com/gardener/controller-manager-library/pkg/resources/abstract"
"github.com/gardener/controller-manager-library/pkg/utils"

api "github.com/gardener/cert-management/pkg/apis/cert/v1alpha1"
certutils "github.com/gardener/cert-management/pkg/cert/utils"
ctrl "github.com/gardener/cert-management/pkg/controller"
)

// SrcReconciler create a source reconciler.
// SrcReconciler creates a source reconciler.
func SrcReconciler(sourceType CertSourceType, rtype controller.ReconcilerType) controller.ReconcilerType {
return func(c controller.Interface) (reconcile.Interface, error) {
s, err := sourceType.Create(c)
Expand Down Expand Up @@ -100,19 +101,23 @@ func (r *sourceReconciler) Setup() error {
return r.NestedReconciler.Setup()
}

func getCertificateSecretName(obj resources.Object) (string, error) {
func getCertificateSecretName(obj resources.Object) (types.NamespacedName, error) {
crt := certutils.Certificate(obj).Certificate()
if crt.Spec.SecretRef != nil {
return crt.Spec.SecretRef.Name, nil
ns := crt.Spec.SecretRef.Namespace
if ns == "" {
ns = obj.GetNamespace()
}
return types.NamespacedName{Namespace: ns, Name: crt.Spec.SecretRef.Name}, nil
} else if crt.Spec.SecretName != nil {
return *crt.Spec.SecretName, nil
return types.NamespacedName{Namespace: obj.GetNamespace(), Name: *crt.Spec.SecretName}, nil
}
return "", fmt.Errorf("missing secret name for %s", obj.GetName())
return types.NamespacedName{}, fmt.Errorf("missing secret name for %s", obj.GetName())
}

func (r *sourceReconciler) Reconcile(logger logger.LogContext, obj resources.Object) reconcile.Status {
slaves := r.LookupSlaves(obj.ClusterKey())
currentState := &CertCurrentState{CertStates: map[string]*CertState{}}
currentState := &CertCurrentState{CertStates: map[types.NamespacedName]*CertState{}}
for _, s := range slaves {
crt := certutils.Certificate(s).Certificate()
secretName, err := getCertificateSecretName(s)
Expand Down Expand Up @@ -147,30 +152,30 @@ func (r *sourceReconciler) Reconcile(logger logger.LogContext, obj resources.Obj
}
}

missingSecretNames := utils.StringSet{}
missingSecretNames := sets.New[types.NamespacedName]()
for secretName := range info.Certs {
if !currentState.ContainsSecretName(secretName) {
missingSecretNames.Add(secretName)
missingSecretNames.Insert(secretName)
}
}

obsolete := []resources.Object{}
obsoleteSecretNames := utils.StringSet{}
obsoleteSecretNames := sets.New[types.NamespacedName]()
current := []resources.Object{}
for _, s := range slaves {
secretName, err := getCertificateSecretName(s)
if err != nil {
obsolete = append(obsolete, s)
} else if _, ok := info.Certs[secretName]; !ok {
obsolete = append(obsolete, s)
obsoleteSecretNames.Add(secretName)
obsoleteSecretNames.Insert(secretName)
} else {
current = append(current, s)
}
}

var notifiedErrors []string
modified := map[string]bool{}
modified := map[types.NamespacedName]bool{}
if len(missingSecretNames) > 0 {
logger.Infof("found missing secrets: %s", missingSecretNames)
for secretName := range missingSecretNames {
Expand Down Expand Up @@ -337,13 +342,9 @@ func (r *sourceReconciler) createEntryFor(logger logger.LogContext, obj resource
cert.Spec.IssuerRef = &api.IssuerRef{Name: *info.IssuerName}
}
}
if info.SecretNamespace != nil {
cert.Spec.SecretRef = &core.SecretReference{
Name: info.SecretName,
Namespace: *info.SecretNamespace,
}
} else {
cert.Spec.SecretName = &info.SecretName
cert.Spec.SecretRef = &core.SecretReference{
Name: info.SecretName.Name,
Namespace: info.SecretName.Namespace,
}
if r.namespace == "" {
cert.Namespace = obj.GetNamespace()
Expand Down
11 changes: 7 additions & 4 deletions pkg/controller/issuer/certificate/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -796,15 +796,15 @@ func (r *certReconciler) isRenewalOverdue(cert *x509.Certificate) bool {
func (r *certReconciler) determineSecretRef(namespace string, spec *api.CertificateSpec) (*corev1.SecretReference, error) {
ns := core.NormalizeNamespace(namespace)
if spec.SecretRef != nil {
if spec.SecretRef.Namespace != "" && spec.SecretRef.Namespace != ns {
return nil, fmt.Errorf("secretRef must be located in same namespace as certificate for security reasons")
}
if spec.SecretRef.Name == "" {
return nil, fmt.Errorf("secretRef.name must not be empty if specified")
}
if spec.SecretName != nil && *spec.SecretName != spec.SecretRef.Name {
return nil, fmt.Errorf("conflicting names in secretRef.Name and secretName: %s != %s", spec.SecretRef.Name, *spec.SecretName)
}
if spec.SecretRef.Namespace != "" {
ns = spec.SecretRef.Namespace
}
return &corev1.SecretReference{
Name: spec.SecretRef.Name,
Namespace: ns,
Expand Down Expand Up @@ -863,7 +863,7 @@ func (r *certReconciler) copySecretIfNeeded(logctx logger.LogContext, issuerInfo
ns := core.NormalizeNamespace(objectMeta.Namespace)
specSecretRef, _ := r.determineSecretRef(ns, spec)
if specSecretRef != nil && secretRef.Name == specSecretRef.Name &&
(secretRef.Namespace == "" || secretRef.Namespace == ns) {
(secretRef.Namespace == specSecretRef.Namespace || (secretRef.Namespace == "" && specSecretRef.Namespace == ns)) {
return specSecretRef, nil
}
secret, err := r.loadSecret(secretRef)
Expand All @@ -886,6 +886,9 @@ func (r *certReconciler) writeCertificateSecret(logctx logger.LogContext, issuer
secret.SetNamespace(core.NormalizeNamespace(objectMeta.GetNamespace()))
if specSecretRef != nil {
secret.SetName(specSecretRef.Name)
if specSecretRef.Namespace != "" {
secret.SetNamespace(specSecretRef.Namespace)
}
// reuse existing secret (especially keep existing annotations and labels)
obj, err := r.certSecretResources.GetInto1(secret)
if err == nil {
Expand Down
26 changes: 13 additions & 13 deletions pkg/controller/source/gateways/gatewayapi/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
gatewayapisv1 "sigs.k8s.io/gateway-api/apis/v1"
)
Expand Down Expand Up @@ -54,7 +55,7 @@ var _ = Describe("Kubernetes Networking Gateway Handler", func() {
routes = []*gatewayapisv1.HTTPRoute{route1, route2, route3}

log = logger.NewContext("", "TestEnv")
emptyMap = map[string]source.CertInfo{}
emptyMap = map[types.NamespacedName]source.CertInfo{}
standardObjectMeta = metav1.ObjectMeta{
Namespace: "test",
Name: "g1",
Expand All @@ -65,7 +66,7 @@ var _ = Describe("Kubernetes Networking Gateway Handler", func() {
)

var _ = DescribeTable("GetCertsInfo",
func(gateway *gatewayapisv1.Gateway, httpRoutes []*gatewayapisv1.HTTPRoute, expectedMap map[string]source.CertInfo) {
func(gateway *gatewayapisv1.Gateway, httpRoutes []*gatewayapisv1.HTTPRoute, expectedMap map[types.NamespacedName]source.CertInfo) {
handler, err := newGatewaySourceWithRouteLister(&testRouteLister{routes: httpRoutes}, newState())
Expect(err).To(Succeed())

Expand Down Expand Up @@ -340,28 +341,27 @@ func (t testRouteLister) ListHTTPRoutes(gateway *resources.ObjectName) ([]resour
return filtered, nil
}

func singleCertInfo(secretName string, ns *string, names ...string) map[string]source.CertInfo {
func singleCertInfo(secretName string, ns *string, names ...string) map[types.NamespacedName]source.CertInfo {
info := makeCertInfo(secretName, ns, names...)
return toMap(info)
}

func toMap(infos ...source.CertInfo) map[string]source.CertInfo {
result := map[string]source.CertInfo{}
func toMap(infos ...source.CertInfo) map[types.NamespacedName]source.CertInfo {
result := map[types.NamespacedName]source.CertInfo{}
for _, info := range infos {
key := info.SecretName
if info.SecretNamespace != nil {
key = *info.SecretNamespace + "/" + info.SecretName
}
result[key] = info
result[info.SecretName] = info
}
return result
}

func makeCertInfo(secretName string, ns *string, names ...string) source.CertInfo {
namespace := "test"
if ns != nil {
namespace = *ns
}
return source.CertInfo{
SecretName: secretName,
SecretNamespace: ns,
Domains: names,
SecretName: types.NamespacedName{Name: secretName, Namespace: namespace},
Domains: names,
}
}

Expand Down
Loading

0 comments on commit 23ece9a

Please sign in to comment.