Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Firewall bootstrap rotation #67

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/v2/types_firewallmonitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions controllers/firewall/monitor.go
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -67,3 +81,79 @@

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)

Check failure on line 101 in controllers/firewall/monitor.go

View workflow job for this annotation

GitHub Actions / Docker Build

odd number of arguments passed as key-value pairs for logging (loggercheck)
}

r.Log.Info("rotate bootstrap token for firewall deployment %q", client.ObjectKeyFromObject(r.Target))

Check failure on line 104 in controllers/firewall/monitor.go

View workflow job for this annotation

GitHub Actions / Docker Build

odd number of arguments passed as key-value pairs for logging (loggercheck)
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
}
5 changes: 5 additions & 0 deletions controllers/firewall/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}()

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading