diff --git a/api/v2/types_firewallmonitor.go b/api/v2/types_firewallmonitor.go index d9bddc6..689f7e1 100644 --- a/api/v2/types_firewallmonitor.go +++ b/api/v2/types_firewallmonitor.go @@ -10,6 +10,11 @@ import ( const ( // FirewallShootNamespace is the name of the namespace to which the firewall monitor gets deployed and in which the firewall-controller operates FirewallShootNamespace = "firewall" + // FirewallBootstrapTokenIDLabel references the the kubernetes bootstrap token ID for a firewall. + // It is defined on the firewall. + FirewallBootstrapTokenIDLabel = "firewall.metal-stack.io/bootstrap-token-id" + // FirewallBootstrapTokenNextRotationLabel reflects when the next rotation of the bootstrap token can be expected. + FirewallBootstrapTokenNextRotationLabel = "firewall.metal-stack.io/bootstrap-token-next-rotation" ) // +kubebuilder:object:root=true diff --git a/controllers/firewall/monitor.go b/controllers/firewall/monitor.go index f7477cf..da2f39b 100644 --- a/controllers/firewall/monitor.go +++ b/controllers/firewall/monitor.go @@ -1,15 +1,29 @@ package firewall import ( + "crypto/rand" "fmt" + "math/big" + "time" v2 "github.com/metal-stack/firewall-controller-manager/api/v2" "github.com/metal-stack/firewall-controller-manager/controllers" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + bootstraptokenapi "k8s.io/cluster-bootstrap/token/api" + bootstraptokenutil "k8s.io/cluster-bootstrap/token/util" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +const ( + characterSetResourceNameFragment = "abcdefghijklmnopqrstuvwxyz0123456789" + firewallBootstrapTokenExpiration = 20 * time.Minute + firewallBootstrapTokenRotationPeriod = 15 * time.Minute + // Extra groups to authenticate the token as. Must start with "system:bootstrappers:" + firewallBootstrapTokenAuthExtraGroups = "" +) + func (c *controller) ensureFirewallMonitor(r *controllers.Ctx[*v2.Firewall]) (*v2.FirewallMonitor, error) { var err error @@ -67,3 +81,79 @@ func (c *controller) ensureFirewallMonitor(r *controllers.Ctx[*v2.Firewall]) (*v return mon, nil } + +func (c *controller) rotateFirewallBootstrapTokenIfNeeded(r *controllers.Ctx[*v2.Firewall]) error { + var ( + tokenID = r.Target.Labels[v2.FirewallBootstrapTokenIDLabel] + nextRotationStr = r.Target.Labels[v2.FirewallBootstrapTokenNextRotationLabel] + ) + + if tokenID == "" || nextRotationStr == "" { + return nil + } + + nextRotation, err := time.Parse(time.RFC3339, nextRotationStr) + if err == nil && (nextRotation.Equal(time.Now()) || nextRotation.Before(time.Now())) { + r.Log.Info("bootstrap token rotation not needed") + return nil + } + if err != nil { + r.Log.Info("invalid firewall bootstrap token interval, %s", err) + } + + r.Log.Info("rotate bootstrap token for firewall deployment %q", client.ObjectKeyFromObject(r.Target)) + tokenID, err = generateNewTokenID() + if err != nil { + return err + } + nextRotation = time.Now().Add(firewallBootstrapTokenRotationPeriod) + nextRotationStr = nextRotation.Format(time.RFC3339) + + r.Target.Labels[v2.FirewallBootstrapTokenNextRotationLabel] = nextRotationStr + r.Target.Labels[v2.FirewallBootstrapTokenIDLabel] = tokenID + + secretName := bootstraptokenutil.BootstrapTokenSecretName(tokenID) + bootstrapTokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: metav1.NamespaceSystem, + }, + } + tokenSecret, err := generateRandomStringFromCharset(16, characterSetResourceNameFragment) + if err != nil { + return err + } + + bootstrapTokenSecret.Data = map[string][]byte{ + bootstraptokenapi.BootstrapTokenIDKey: []byte(tokenID), + bootstraptokenapi.BootstrapTokenSecretKey: []byte(tokenSecret), + bootstraptokenapi.BootstrapTokenExpirationKey: []byte(metav1.Now().Add(firewallBootstrapTokenExpiration).Format(time.RFC3339)), + bootstraptokenapi.BootstrapTokenDescriptionKey: []byte(fmt.Sprintf("Bootstrap token for firewall deployment %s/%s", r.Target.GetNamespace(), r.Target.GetName())), + bootstraptokenapi.BootstrapTokenUsageAuthentication: []byte("true"), + bootstraptokenapi.BootstrapTokenUsageSigningKey: []byte("true"), + bootstraptokenapi.BootstrapTokenExtraGroupsKey: []byte(firewallBootstrapTokenAuthExtraGroups), + } + err = c.c.GetSeedClient().Create(r.Ctx, bootstrapTokenSecret) + if err != nil { + return err + } + + return c.c.GetSeedClient().Update(r.Ctx, r.Target) +} + +func generateNewTokenID() (string, error) { + return generateRandomStringFromCharset(6, characterSetResourceNameFragment) +} + +func generateRandomStringFromCharset(n int, allowedCharacters string) (string, error) { + output := make([]byte, n) + maximum := new(big.Int).SetInt64(int64(len(allowedCharacters))) + for i := range output { + randomCharacter, err := rand.Int(rand.Reader, maximum) + if err != nil { + return "", err + } + output[i] = allowedCharacters[randomCharacter.Int64()] + } + return string(output), nil +} diff --git a/controllers/firewall/reconcile.go b/controllers/firewall/reconcile.go index 89c07bc..794e2e2 100644 --- a/controllers/firewall/reconcile.go +++ b/controllers/firewall/reconcile.go @@ -29,6 +29,11 @@ func (c *controller) Reconcile(r *controllers.Ctx[*v2.Firewall]) error { r.Log.Error(err, "unable to deploy firewall monitor") } + err = c.rotateFirewallBootstrapTokenIfNeeded(r) + if err != nil { + r.Log.Error(err, "unable to rotate firewall bootstrap token") + } + SetFirewallStatusFromMonitor(r.Target, mon) }() diff --git a/go.mod b/go.mod index bdfabd7..9bddced 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 + k8s.io/cluster-bootstrap v0.29.3 sigs.k8s.io/controller-runtime v0.16.5 ) diff --git a/go.sum b/go.sum index 9dc1ded..e629117 100644 --- a/go.sum +++ b/go.sum @@ -551,6 +551,8 @@ k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= +k8s.io/cluster-bootstrap v0.29.3 h1:DIMDZSN8gbFMy9CS2mAS2Iqq/fIUG783WN/1lqi5TF8= +k8s.io/cluster-bootstrap v0.29.3/go.mod h1:aPAg1VtXx3uRrx5qU2jTzR7p1rf18zLXWS+pGhiqPto= k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=