153 lines
4.8 KiB
Go
153 lines
4.8 KiB
Go
// 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
|
||
|
|
}
|