kube,ipn/store/kubestore: allow patching empty state Secrets

Allow users to pre-create empty state Secrets

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
pull/11326/head
Irbe Krumina 3 months ago
parent 52d2595f88
commit 6b80be2f2f

@ -81,46 +81,47 @@ var kc kube.Client
// ensure that tailscale state storage and authentication mechanism will work on // ensure that tailscale state storage and authentication mechanism will work on
// Kubernetes. // Kubernetes.
func (cfg *settings) setupKube(ctx context.Context) error { func (cfg *settings) setupKube(ctx context.Context) error {
if cfg.KubeSecret != "" { if cfg.KubeSecret == "" {
canPatch, canCreate, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret) return nil
if err != nil { }
return fmt.Errorf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err) canPatch, canCreate, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret)
} if err != nil {
cfg.KubernetesCanPatch = canPatch return fmt.Errorf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err)
}
cfg.KubernetesCanPatch = canPatch
s, err := kc.GetSecret(ctx, cfg.KubeSecret) s, err := kc.GetSecret(ctx, cfg.KubeSecret)
if err != nil && kube.IsNotFoundErr(err) && !canCreate { if err != nil && kube.IsNotFoundErr(err) && !canCreate {
return fmt.Errorf("Tailscale state Secret %s does not exist and we don't have permissions to create it. "+ return fmt.Errorf("Tailscale state Secret %s does not exist and we don't have permissions to create it. "+
"If you intend to store tailscale state elsewhere than a Kubernetes Secret, "+ "If you intend to store tailscale state elsewhere than a Kubernetes Secret, "+
"you can explicitly set TS_KUBE_SECRET env var to an empty string. "+ "you can explicitly set TS_KUBE_SECRET env var to an empty string. "+
"Else ensure that RBAC is set up that allows the service account associated with this installation to create Secrets.", cfg.KubeSecret) "Else ensure that RBAC is set up that allows the service account associated with this installation to create Secrets.", cfg.KubeSecret)
} else if err != nil && !kube.IsNotFoundErr(err) { } else if err != nil && !kube.IsNotFoundErr(err) {
return fmt.Errorf("Getting Tailscale state Secret %s: %v", cfg.KubeSecret, err) return fmt.Errorf("Getting Tailscale state Secret %s: %v", cfg.KubeSecret, err)
} }
if cfg.AuthKey == "" && !isOneStepConfig(cfg) { if cfg.AuthKey == "" && !isOneStepConfig(cfg) {
if s == nil { if s == nil {
log.Print("TS_AUTHKEY not provided and kube secret does not exist, login will be interactive if needed.") log.Print("TS_AUTHKEY not provided and kube secret does not exist, login will be interactive if needed.")
return nil return nil
} }
keyBytes, _ := s.Data["authkey"] keyBytes, _ := s.Data["authkey"]
key := string(keyBytes) key := string(keyBytes)
if key != "" { if key != "" {
// This behavior of pulling authkeys from kube secrets was added // This behavior of pulling authkeys from kube secrets was added
// at the same time as the patch permission, so we can enforce // at the same time as the patch permission, so we can enforce
// that we must be able to patch out the authkey after // that we must be able to patch out the authkey after
// authenticating if you want to use this feature. This avoids // authenticating if you want to use this feature. This avoids
// us having to deal with the case where we might leave behind // us having to deal with the case where we might leave behind
// an unnecessary reusable authkey in a secret, like a rake in // an unnecessary reusable authkey in a secret, like a rake in
// the grass. // the grass.
if !cfg.KubernetesCanPatch { if !cfg.KubernetesCanPatch {
return errors.New("authkey found in TS_KUBE_SECRET, but the pod doesn't have patch permissions on the secret to manage the authkey.") return errors.New("authkey found in TS_KUBE_SECRET, but the pod doesn't have patch permissions on the secret to manage the authkey.")
}
cfg.AuthKey = key
} else {
log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.")
} }
cfg.AuthKey = key
} else {
log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.")
} }
} }
return nil return nil

@ -7,6 +7,7 @@ package kubestore
import ( import (
"context" "context"
"fmt"
"net" "net"
"strings" "strings"
"time" "time"
@ -100,6 +101,19 @@ func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
return err return err
} }
if s.canPatch { if s.canPatch {
if len(secret.Data) == 0 { // if user has pre-created a blank Secret
m := []kube.JSONPatch{
{
Op: "add",
Path: "/data",
Value: map[string][]byte{sanitizeKey(id): bs},
},
}
if err := s.client.JSONPatchSecret(ctx, s.secretName, m); err != nil {
return fmt.Errorf("error patching Secret %s with a /data field: %v", s.secretName, err)
}
return nil
}
m := []kube.JSONPatch{ m := []kube.JSONPatch{
{ {
Op: "add", Op: "add",
@ -108,7 +122,7 @@ func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
}, },
} }
if err := s.client.JSONPatchSecret(ctx, s.secretName, m); err != nil { if err := s.client.JSONPatchSecret(ctx, s.secretName, m); err != nil {
return err return fmt.Errorf("error patching Secret %s with /data/%s field", s.secretName, sanitizeKey(id))
} }
return nil return nil
} }

@ -240,7 +240,7 @@ func (c *client) UpdateSecret(ctx context.Context, s *Secret) error {
} }
// JSONPatch is a JSON patch operation. // JSONPatch is a JSON patch operation.
// It currently (2023-03-02) only supports the "remove" operation. // It currently (2023-03-02) only supports "add" and "remove" operations.
// //
// https://tools.ietf.org/html/rfc6902 // https://tools.ietf.org/html/rfc6902
type JSONPatch struct { type JSONPatch struct {
@ -278,7 +278,7 @@ func (c *client) StrategicMergePatchSecret(ctx context.Context, name string, s *
// CheckSecretPermissions checks the secret access permissions of the current // CheckSecretPermissions checks the secret access permissions of the current
// pod. It returns an error if the basic permissions tailscale needs are // pod. It returns an error if the basic permissions tailscale needs are
// missing, and reports whether the patch permission is additionally present. // missing, and reports whether the patch and create permissions are additionally present.
// //
// Errors encountered during the access checking process are logged, but ignored // Errors encountered during the access checking process are logged, but ignored
// so that the pod tries to fail alive if the permissions exist and there's just // so that the pod tries to fail alive if the permissions exist and there's just

Loading…
Cancel
Save