From 74bcaeaf80a14bd30b72a8dcdec19bec71cdf791 Mon Sep 17 00:00:00 2001 From: Haiyan Meng Date: Thu, 13 Aug 2026 21:00:30 -0400 Subject: [PATCH] Add EgressMITMTrustReconciler It Watches ate-system/egress-mitm-ca-pool, unmarshals it with localca.Unmarshal, and server-side-applies a ClusterTrustBundle whose spec.trustBundle is the PEM concatenation of every CA's RootCertificate.Raw, in pool order, and nothing else. --- .../controllers/egressmitmtrust_controller.go | 183 ++++++++++ .../egressmitmtrust_controller_test.go | 328 ++++++++++++++++++ cmd/atecontroller/main.go | 24 ++ manifests/ate-install/generated/role.yaml | 20 ++ 4 files changed, 555 insertions(+) create mode 100644 cmd/atecontroller/internal/controllers/egressmitmtrust_controller.go create mode 100644 cmd/atecontroller/internal/controllers/egressmitmtrust_controller_test.go diff --git a/cmd/atecontroller/internal/controllers/egressmitmtrust_controller.go b/cmd/atecontroller/internal/controllers/egressmitmtrust_controller.go new file mode 100644 index 000000000..f2e8f87c0 --- /dev/null +++ b/cmd/atecontroller/internal/controllers/egressmitmtrust_controller.go @@ -0,0 +1,183 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 controllers + +import ( + "context" + "encoding/pem" + "fmt" + "strings" + + certsv1beta1 "k8s.io/api/certificates/v1beta1" + corev1 "k8s.io/api/core/v1" + k8errors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + certsv1beta1ac "k8s.io/client-go/applyconfigurations/certificates/v1beta1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/agent-substrate/substrate/internal/localca" +) + +// EgressMITMTrustReconciler derives a ClusterTrustBundle from the egress MITM +// CA pool. +type EgressMITMTrustReconciler struct { + client.Client +} + +// EgressMITMCAPoolRef names the Secret holding the CA pool the egress gateway's +// sdsmint sidecar signs per-SNI leaves with. +func EgressMITMCAPoolRef() types.NamespacedName { + const egressMITMCAPoolSecret = "egress-mitm-ca-pool" + return types.NamespacedName{Namespace: ateSystemNamespace, Name: egressMITMCAPoolSecret} +} + +//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch +//+kubebuilder:rbac:groups=certificates.k8s.io,resources=clustertrustbundles,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=certificates.k8s.io,resources=signers,resourceNames=egress-mitm.ate.dev/*,verbs=attest + +func (r *EgressMITMTrustReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + secret := &corev1.Secret{} + if err := r.Get(ctx, req.NamespacedName, secret); err != nil { + if k8errors.IsNotFound(err) { + // The pool is gone, so the anchor goes with it. A ClusterTrustBundle + // cannot take an OwnerReference on a namespaced Secret -- cluster-scoped + // objects may not be owned by namespaced ones -- so nothing collects it + // for us, and a bundle left behind keeps every consumer trusting a CA + // that no longer has an owner but whose key may still exist somewhere. + return ctrl.Result{}, r.deleteTrustBundle(ctx) + } + return ctrl.Result{}, fmt.Errorf("failed to get egress MITM CA pool %q: %w", req.NamespacedName, err) + } + if !secret.GetDeletionTimestamp().IsZero() { + return ctrl.Result{}, r.deleteTrustBundle(ctx) + } + + trustBundle, err := egressMITMTrustBundlePEM(secret) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to derive the egress MITM trust bundle from %q: %w", req.NamespacedName, err) + } + + ctbAC := buildEgressMITMTrustBundleApplyConfig(trustBundle) + + // Server-side apply rather than get-then-update: it creates and updates + // through one call, and it reverts hand edits to the fields owned here + // without clobbering anything a different manager legitimately set. + const egressMITMTrustFieldOwner = "ate-egress-mitm-trust" + if err := r.Apply(ctx, ctbAC, client.FieldOwner(egressMITMTrustFieldOwner), client.ForceOwnership); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to apply ClusterTrustBundle %q: %w", *ctbAC.Name, err) + } + log.Info("reconciled the egress MITM trust bundle", + "name", *ctbAC.Name, + "secret", req.NamespacedName.String()) + + return ctrl.Result{}, nil +} + +// egressMITMSignerName identifies the MITM CA's trust domain. +const egressMITMSignerName = "egress-mitm.ate.dev/mitm" + +const egressMITMTrustBundleName = "egress-mitm.ate.dev:mitm:primary-bundle" + +func buildEgressMITMTrustBundleApplyConfig(trustBundle string) *certsv1beta1ac.ClusterTrustBundleApplyConfiguration { + return certsv1beta1ac.ClusterTrustBundle(egressMITMTrustBundleName). + WithLabels(map[string]string{ + "podcert.ate.dev/canarying": "live", + }). + WithSpec(certsv1beta1ac.ClusterTrustBundleSpec(). + WithSignerName(egressMITMSignerName). + WithTrustBundle(trustBundle)) +} + +// egressMITMTrustBundlePEM renders the pool's root certificates, and nothing +// else, as the PEM blob a ClusterTrustBundle carries. +func egressMITMTrustBundlePEM(secret *corev1.Secret) (string, error) { + // The key `kubectl-ate admin make-ca-pool` writes the marshaled pool under. + const egressMITMCAPoolKey = "pool" + + wire, ok := secret.Data[egressMITMCAPoolKey] + if !ok { + return "", fmt.Errorf("secret has no %q key", egressMITMCAPoolKey) + } + pool, err := localca.Unmarshal(wire) + if err != nil { + return "", fmt.Errorf("parsing the CA pool: %w", err) + } + if len(pool.CAs) == 0 { + return "", fmt.Errorf("the CA pool contains no CAs") + } + + var b strings.Builder + for _, ca := range pool.CAs { + if ca.RootCertificate == nil { + return "", fmt.Errorf("CA %q has no root certificate", ca.ID) + } + if err := pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: ca.RootCertificate.Raw}); err != nil { + return "", fmt.Errorf("encoding the root certificate of CA %q: %w", ca.ID, err) + } + } + return b.String(), nil +} + +func (r *EgressMITMTrustReconciler) deleteTrustBundle(ctx context.Context) error { + log := log.FromContext(ctx) + + ctb := &certsv1beta1.ClusterTrustBundle{} + if err := r.Get(ctx, types.NamespacedName{Name: egressMITMTrustBundleName}, ctb); err != nil { + if k8errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("failed to get ClusterTrustBundle %q: %w", egressMITMTrustBundleName, err) + } + + if ctb.Spec.SignerName != egressMITMSignerName { + return fmt.Errorf("refusing to delete ClusterTrustBundle %q: signer is %q, not %q", + egressMITMTrustBundleName, ctb.Spec.SignerName, egressMITMSignerName) + } + + if err := r.Delete(ctx, ctb, client.Preconditions{UID: &ctb.UID}); err != nil && !k8errors.IsNotFound(err) { + return fmt.Errorf("failed to delete ClusterTrustBundle %q: %w", egressMITMTrustBundleName, err) + } + log.Info("deleted the egress MITM trust bundle; its CA pool is gone", "name", egressMITMTrustBundleName) + return nil +} + +func (r *EgressMITMTrustReconciler) SetupWithManager(mgr ctrl.Manager) error { + poolRef := EgressMITMCAPoolRef() + + // The pool Secret is the only object reconciled from. The bundle is watched + // as well so that deleting or hand-editing the derived object is reverted + // rather than silently accepted. + return ctrl.NewControllerManagedBy(mgr). + Named("egressmitmtrust"). + For(&corev1.Secret{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(obj client.Object) bool { + return obj.GetNamespace() == poolRef.Namespace && obj.GetName() == poolRef.Name + }))). + Watches(&certsv1beta1.ClusterTrustBundle{}, + handler.EnqueueRequestsFromMapFunc(func(context.Context, client.Object) []reconcile.Request { + return []reconcile.Request{{NamespacedName: poolRef}} + }), + builder.WithPredicates(predicate.NewPredicateFuncs(func(obj client.Object) bool { + return obj.GetName() == egressMITMTrustBundleName + }))). + Complete(r) +} diff --git a/cmd/atecontroller/internal/controllers/egressmitmtrust_controller_test.go b/cmd/atecontroller/internal/controllers/egressmitmtrust_controller_test.go new file mode 100644 index 000000000..a02a82974 --- /dev/null +++ b/cmd/atecontroller/internal/controllers/egressmitmtrust_controller_test.go @@ -0,0 +1,328 @@ +// Copyright 2026 Google LLC +// +// Licensed 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 controllers + +import ( + "context" + "crypto/x509" + "encoding/pem" + "strings" + "testing" + + certsv1beta1 "k8s.io/api/certificates/v1beta1" + corev1 "k8s.io/api/core/v1" + k8errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/agent-substrate/substrate/internal/localca" +) + +func egressMITMScheme(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatalf("add scheme: %v", err) + } + return scheme +} + +// caPoolSecret marshals ids into a pool Secret shaped the way +// `kubectl-ate admin make-ca-pool` writes it, and returns the roots in pool +// order so a test can assert on exactly what should have been published. +func caPoolSecret(t *testing.T, ids ...string) (*corev1.Secret, *localca.Pool) { + t.Helper() + pool := &localca.Pool{} + for _, id := range ids { + ca, err := localca.GenerateED25519CA(id) + if err != nil { + t.Fatalf("generate CA %q: %v", id, err) + } + pool.CAs = append(pool.CAs, ca) + } + return secretForPool(t, pool), pool +} + +func secretForPool(t *testing.T, pool *localca.Pool) *corev1.Secret { + t.Helper() + wire, err := localca.Marshal(pool) + if err != nil { + t.Fatalf("marshal CA pool: %v", err) + } + ref := EgressMITMCAPoolRef() + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: ref.Namespace, Name: ref.Name}, + Data: map[string][]byte{"pool": wire}, + } +} + +func rootPEM(t *testing.T, pool *localca.Pool) string { + t.Helper() + var b strings.Builder + for _, ca := range pool.CAs { + if err := pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: ca.RootCertificate.Raw}); err != nil { + t.Fatalf("encode root: %v", err) + } + } + return b.String() +} + +func reconcilePool(t *testing.T, c client.Client) error { + t.Helper() + r := &EgressMITMTrustReconciler{Client: c} + _, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: EgressMITMCAPoolRef()}) + return err +} + +func getTrustBundle(t *testing.T, c client.Client) (*certsv1beta1.ClusterTrustBundle, bool) { + t.Helper() + ctb := &certsv1beta1.ClusterTrustBundle{} + err := c.Get(context.Background(), types.NamespacedName{Name: egressMITMTrustBundleName}, ctb) + if k8errors.IsNotFound(err) { + return nil, false + } + if err != nil { + t.Fatalf("get ClusterTrustBundle: %v", err) + } + return ctb, true +} + +func TestEgressMITMTrustPublishesEveryRoot(t *testing.T) { + t.Parallel() + scheme := egressMITMScheme(t) + secret, pool := caPoolSecret(t, "mitm", "mitm-next") + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + + if err := reconcilePool(t, c); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + ctb, ok := getTrustBundle(t, c) + if !ok { + t.Fatal("no ClusterTrustBundle was created") + } + if got, want := ctb.Spec.SignerName, egressMITMSignerName; got != want { + t.Errorf("signerName = %q, want %q", got, want) + } + if got, want := ctb.Labels["podcert.ate.dev/canarying"], "live"; got != want { + t.Errorf("canarying label = %q, want %q", got, want) + } + // Both roots, in pool order. A pool holds more than one CA so the anchor can + // be rotated; dropping the outgoing root breaks leaves it is still signing. + if got, want := ctb.Spec.TrustBundle, rootPEM(t, pool); got != want { + t.Errorf("trustBundle =\n%s\nwant\n%s", got, want) + } +} + +// The pool carries each CA's signing key. Publishing it would hand every +// consumer of the bundle the ability to mint leaves for any name. +func TestEgressMITMTrustPublishesNoPrivateKey(t *testing.T) { + t.Parallel() + scheme := egressMITMScheme(t) + secret, _ := caPoolSecret(t, "mitm") + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + + if err := reconcilePool(t, c); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + ctb, ok := getTrustBundle(t, c) + if !ok { + t.Fatal("no ClusterTrustBundle was created") + } + for rest := []byte(ctb.Spec.TrustBundle); len(rest) > 0; { + var block *pem.Block + block, rest = pem.Decode(rest) + if block == nil { + t.Fatalf("trustBundle has trailing non-PEM bytes: %q", rest) + } + if block.Type != "CERTIFICATE" { + t.Errorf("trustBundle contains a %q block; only CERTIFICATE belongs there", block.Type) + } + } +} + +// An intermediate in a trust bundle is an anchor, not a link in a chain: +// publishing one would make everything it ever signed directly trusted. +func TestEgressMITMTrustOmitsIntermediates(t *testing.T) { + t.Parallel() + scheme := egressMITMScheme(t) + _, pool := caPoolSecret(t, "mitm") + _, other := caPoolSecret(t, "not-an-anchor") + pool.CAs[0].IntermediateCertificates = []*x509.Certificate{other.CAs[0].RootCertificate} + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secretForPool(t, pool)).Build() + + if err := reconcilePool(t, c); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + ctb, ok := getTrustBundle(t, c) + if !ok { + t.Fatal("no ClusterTrustBundle was created") + } + if got, want := ctb.Spec.TrustBundle, rootPEM(t, pool); got != want { + t.Errorf("trustBundle =\n%s\nwant only the root\n%s", got, want) + } + if strings.Contains(ctb.Spec.TrustBundle, rootPEM(t, other)) { + t.Error("trustBundle contains the intermediate; only roots belong there") + } +} + +func TestEgressMITMTrustFollowsPoolRotation(t *testing.T) { + t.Parallel() + scheme := egressMITMScheme(t) + secret, _ := caPoolSecret(t, "mitm") + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + if err := reconcilePool(t, c); err != nil { + t.Fatalf("first Reconcile: %v", err) + } + + rotated, rotatedPool := caPoolSecret(t, "mitm", "mitm-next") + current := &corev1.Secret{} + if err := c.Get(context.Background(), EgressMITMCAPoolRef(), current); err != nil { + t.Fatalf("get pool secret: %v", err) + } + current.Data = rotated.Data + if err := c.Update(context.Background(), current); err != nil { + t.Fatalf("update pool secret: %v", err) + } + + if err := reconcilePool(t, c); err != nil { + t.Fatalf("second Reconcile: %v", err) + } + ctb, ok := getTrustBundle(t, c) + if !ok { + t.Fatal("the ClusterTrustBundle disappeared") + } + if got, want := ctb.Spec.TrustBundle, rootPEM(t, rotatedPool); got != want { + t.Errorf("trustBundle after rotation =\n%s\nwant\n%s", got, want) + } +} + +func TestEgressMITMTrustRecreatesDeletedBundle(t *testing.T) { + t.Parallel() + scheme := egressMITMScheme(t) + secret, pool := caPoolSecret(t, "mitm") + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + if err := reconcilePool(t, c); err != nil { + t.Fatalf("first Reconcile: %v", err) + } + ctb, ok := getTrustBundle(t, c) + if !ok { + t.Fatal("no ClusterTrustBundle was created") + } + if err := c.Delete(context.Background(), ctb); err != nil { + t.Fatalf("delete ClusterTrustBundle: %v", err) + } + + if err := reconcilePool(t, c); err != nil { + t.Fatalf("second Reconcile: %v", err) + } + got, ok := getTrustBundle(t, c) + if !ok { + t.Fatal("the ClusterTrustBundle was not recreated") + } + if want := rootPEM(t, pool); got.Spec.TrustBundle != want { + t.Errorf("trustBundle =\n%s\nwant\n%s", got.Spec.TrustBundle, want) + } +} + +func TestEgressMITMTrustDeletesBundleWhenPoolIsGone(t *testing.T) { + t.Parallel() + scheme := egressMITMScheme(t) + secret, _ := caPoolSecret(t, "mitm") + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + if err := reconcilePool(t, c); err != nil { + t.Fatalf("first Reconcile: %v", err) + } + if err := c.Delete(context.Background(), secret); err != nil { + t.Fatalf("delete pool secret: %v", err) + } + + if err := reconcilePool(t, c); err != nil { + t.Fatalf("second Reconcile: %v", err) + } + if _, ok := getTrustBundle(t, c); ok { + t.Error("the ClusterTrustBundle outlived its CA pool") + } +} + +func TestEgressMITMTrustLeavesForeignBundleAlone(t *testing.T) { + t.Parallel() + scheme := egressMITMScheme(t) + foreign := &certsv1beta1.ClusterTrustBundle{ + ObjectMeta: metav1.ObjectMeta{Name: egressMITMTrustBundleName}, + Spec: certsv1beta1.ClusterTrustBundleSpec{SignerName: "someone.else.example/identity"}, + } + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(foreign).Build() + + // No pool Secret exists, so this takes the delete path. + if err := reconcilePool(t, c); err == nil { + t.Fatal("Reconcile deleted a ClusterTrustBundle belonging to another signer") + } + if _, ok := getTrustBundle(t, c); !ok { + t.Error("the foreign ClusterTrustBundle was deleted") + } +} + +// A pool that cannot be read says nothing about what the anchor should be. +// Truncating the published bundle would break every consumer at once, so the +// last good bundle has to survive an unreadable pool. +func TestEgressMITMTrustKeepsLastGoodBundleOnBadPool(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + data map[string][]byte + }{ + {name: "missing key", data: map[string][]byte{"not-pool": []byte("{}")}}, + {name: "unparseable", data: map[string][]byte{"pool": []byte("not json")}}, + {name: "no CAs", data: map[string][]byte{"pool": []byte(`{"CAs":[]}`)}}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + scheme := egressMITMScheme(t) + secret, pool := caPoolSecret(t, "mitm") + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + if err := reconcilePool(t, c); err != nil { + t.Fatalf("first Reconcile: %v", err) + } + + current := &corev1.Secret{} + if err := c.Get(context.Background(), EgressMITMCAPoolRef(), current); err != nil { + t.Fatalf("get pool secret: %v", err) + } + current.Data = tc.data + if err := c.Update(context.Background(), current); err != nil { + t.Fatalf("update pool secret: %v", err) + } + + if err := reconcilePool(t, c); err == nil { + t.Error("Reconcile accepted an unreadable pool; it should fail and requeue") + } + ctb, ok := getTrustBundle(t, c) + if !ok { + t.Fatal("the ClusterTrustBundle was removed by an unreadable pool") + } + if want := rootPEM(t, pool); ctb.Spec.TrustBundle != want { + t.Errorf("trustBundle was rewritten from an unreadable pool:\n%s", ctb.Spec.TrustBundle) + } + }) + } +} diff --git a/cmd/atecontroller/main.go b/cmd/atecontroller/main.go index ee1cdfe42..ba72f5e1a 100644 --- a/cmd/atecontroller/main.go +++ b/cmd/atecontroller/main.go @@ -28,11 +28,15 @@ import ( prombridge "go.opentelemetry.io/contrib/bridges/prometheus" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" @@ -139,8 +143,21 @@ func main() { ateapiClient := ateapipb.NewControlClient(ateapiConn) + // EgressMITMTrustReconciler watches the Secret `egress-mitm-ca-pool`. + egressMITMCAPool := controllers.EgressMITMCAPoolRef() mgr, err := ctrl.NewManager(k8sConfig, ctrl.Options{ Scheme: scheme, + Cache: cache.Options{ + ByObject: map[client.Object]cache.ByObject{ + &corev1.Secret{}: { + Namespaces: map[string]cache.Config{ + egressMITMCAPool.Namespace: { + FieldSelector: fields.OneTermEqualSelector("metadata.name", egressMITMCAPool.Name), + }, + }, + }, + }, + }, }) if err != nil { setupLog.Error(err, "unable to start manager") @@ -177,6 +194,13 @@ func main() { os.Exit(1) } + if err = (&controllers.EgressMITMTrustReconciler{ + Client: mgr.GetClient(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "EgressMITMTrust") + os.Exit(1) + } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/manifests/ate-install/generated/role.yaml b/manifests/ate-install/generated/role.yaml index b99d71e49..35dbfa305 100644 --- a/manifests/ate-install/generated/role.yaml +++ b/manifests/ate-install/generated/role.yaml @@ -80,6 +80,26 @@ rules: - get - patch - update +- apiGroups: + - certificates.k8s.io + resources: + - clustertrustbundles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - certificates.k8s.io + resourceNames: + - egress-mitm.ate.dev/* + resources: + - signers + verbs: + - attest - apiGroups: - networking.k8s.io resources: