feat(iam): define a KeyRotation for every consumer except poimen-memory
Eight objects cover grafana, argocd, comfyui, minio, paperless, immich, forgejo and api-gw. Each names its ZITADEL app, the Secret it writes, the consumer to roll and whether the consumer reloads hot or needs a restart. All are suspended: the controller image does not exist yet, and an unsuspended object would claim an intent that no process acts on. The README records the retirement target for the 34 .enc.yaml files: six root-of-trust secrets that cannot rotate themselves survive, everything else becomes a KeyRotation and leaves git.
This commit was merged in pull request #84.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
// Command key-rotation-controller rotates credentials declared by KeyRotation
|
||||
// objects.
|
||||
//
|
||||
// The controller authenticates to ZITADEL with its own machine key, read from a
|
||||
// mounted Secret. That key is the single bootstrap credential: everything else is
|
||||
// created by this process and never committed to git.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
iamv1 "github.com/riotpiao/key-rotation-controller/api"
|
||||
keyreconcile "github.com/riotpiao/key-rotation-controller/internal/reconcile"
|
||||
"github.com/riotpiao/key-rotation-controller/internal/zitadel"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
metricsAddr = flag.String("metrics-bind-address", ":8080", "address for the metrics endpoint")
|
||||
healthAddr = flag.String("health-probe-bind-address", ":8081", "address for probes")
|
||||
interval = flag.Duration("scan-interval", 10*time.Minute, "how often to re-check every resource")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseDevMode(false)))
|
||||
logger := ctrl.Log.WithName("setup")
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
if err := clientgoscheme.AddToScheme(scheme); err != nil {
|
||||
logger.Error(err, "cannot register the Kubernetes types")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := iamv1.AddToScheme(scheme); err != nil {
|
||||
logger.Error(err, "cannot register the KeyRotation types")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsserver.Options{BindAddress: *metricsAddr},
|
||||
HealthProbeBindAddress: *healthAddr,
|
||||
LeaderElection: true,
|
||||
LeaderElectionID: "key-rotation-controller.iam.homelab.riotpiao.com",
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot create the manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
zitadelClient, err := buildZitadelClient()
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot build the ZITADEL client")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
reconciler := &keyreconcile.Reconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Providers: map[string]keyreconcile.Provider{
|
||||
iamv1.ProviderZitadelMachineKey: &keyreconcile.MachineKeyProvider{
|
||||
Client: zitadelClient,
|
||||
AuthClientID: os.Getenv("ZITADEL_CLIENT_ID"),
|
||||
},
|
||||
iamv1.ProviderZitadelOIDCClient: &keyreconcile.OIDCClientProvider{
|
||||
Client: zitadelClient,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c, err := controller.New("keyrotation", mgr, controller.Options{
|
||||
Reconciler: ctrlreconcile.Func(func(ctx context.Context, req ctrlreconcile.Request) (ctrlreconcile.Result, error) {
|
||||
wait, err := reconciler.Reconcile(ctx, types.NamespacedName(req.NamespacedName))
|
||||
if err != nil {
|
||||
return ctrlreconcile.Result{}, err
|
||||
}
|
||||
if wait <= 0 {
|
||||
wait = *interval
|
||||
}
|
||||
return ctrlreconcile.Result{RequeueAfter: wait}, nil
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot create the controller")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := c.Watch(source.Kind(mgr.GetCache(), client.Object(&iamv1.KeyRotation{}), &handler.EnqueueRequestForObject{})); err != nil {
|
||||
logger.Error(err, "cannot watch KeyRotation objects")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", func(*http.Request) error { return nil }); err != nil {
|
||||
logger.Error(err, "cannot add the health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger.Info("starting the key rotation controller")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
logger.Error(err, "manager stopped")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// buildZitadelClient reads the controller's own credential from the environment.
|
||||
//
|
||||
// These are the only values the controller cannot create for itself. They arrive
|
||||
// from one Secret, created once when the cluster is bootstrapped.
|
||||
func buildZitadelClient() (*zitadel.Client, error) {
|
||||
issuer := os.Getenv("ZITADEL_ISSUER")
|
||||
clientID := os.Getenv("ZITADEL_CLIENT_ID")
|
||||
userID := os.Getenv("ZITADEL_USER_ID")
|
||||
keyID := os.Getenv("ZITADEL_KEY_ID")
|
||||
keyFile := envOr("ZITADEL_KEY_FILE", "/var/run/keys/controller/key")
|
||||
|
||||
if issuer == "" || clientID == "" || keyID == "" {
|
||||
return nil, fmt.Errorf("ZITADEL_ISSUER, ZITADEL_CLIENT_ID and ZITADEL_KEY_ID are required")
|
||||
}
|
||||
pem, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read the controller key: %w", err)
|
||||
}
|
||||
return zitadel.New(zitadel.Config{
|
||||
Issuer: issuer,
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
KeyID: keyID,
|
||||
PrivateKeyPEM: pem,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if v := os.Getenv(name); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user