From 6b80be2f2fcf4861056babab10eb193e6df9f494 Mon Sep 17 00:00:00 2001 From: Irbe Krumina Date: Mon, 4 Mar 2024 13:14:55 +0000 Subject: [PATCH] kube,ipn/store/kubestore: allow patching empty state Secrets Allow users to pre-create empty state Secrets Signed-off-by: Irbe Krumina --- cmd/containerboot/kube.go | 73 ++++++++++++++++--------------- ipn/store/kubestore/store_kube.go | 16 ++++++- kube/client.go | 4 +- 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/cmd/containerboot/kube.go b/cmd/containerboot/kube.go index 92760f95a..921e11fa0 100644 --- a/cmd/containerboot/kube.go +++ b/cmd/containerboot/kube.go @@ -81,46 +81,47 @@ var kc kube.Client // ensure that tailscale state storage and authentication mechanism will work on // Kubernetes. func (cfg *settings) setupKube(ctx context.Context) error { - if cfg.KubeSecret != "" { - canPatch, canCreate, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret) - if err != nil { - return fmt.Errorf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err) - } - cfg.KubernetesCanPatch = canPatch + if cfg.KubeSecret == "" { + return nil + } + canPatch, canCreate, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret) + if err != nil { + 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) - 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. "+ - "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. "+ - "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) { - return fmt.Errorf("Getting Tailscale state Secret %s: %v", cfg.KubeSecret, err) - } + s, err := kc.GetSecret(ctx, cfg.KubeSecret) + 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. "+ + "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. "+ + "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) { + return fmt.Errorf("Getting Tailscale state Secret %s: %v", cfg.KubeSecret, err) + } - if cfg.AuthKey == "" && !isOneStepConfig(cfg) { - if s == nil { - log.Print("TS_AUTHKEY not provided and kube secret does not exist, login will be interactive if needed.") - return nil - } - keyBytes, _ := s.Data["authkey"] - key := string(keyBytes) + if cfg.AuthKey == "" && !isOneStepConfig(cfg) { + if s == nil { + log.Print("TS_AUTHKEY not provided and kube secret does not exist, login will be interactive if needed.") + return nil + } + keyBytes, _ := s.Data["authkey"] + key := string(keyBytes) - if key != "" { - // This behavior of pulling authkeys from kube secrets was added - // at the same time as the patch permission, so we can enforce - // that we must be able to patch out the authkey after - // authenticating if you want to use this feature. This avoids - // us having to deal with the case where we might leave behind - // an unnecessary reusable authkey in a secret, like a rake in - // the grass. - 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.") - } - cfg.AuthKey = key - } else { - log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.") + if key != "" { + // This behavior of pulling authkeys from kube secrets was added + // at the same time as the patch permission, so we can enforce + // that we must be able to patch out the authkey after + // authenticating if you want to use this feature. This avoids + // us having to deal with the case where we might leave behind + // an unnecessary reusable authkey in a secret, like a rake in + // the grass. + 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.") } + 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 diff --git a/ipn/store/kubestore/store_kube.go b/ipn/store/kubestore/store_kube.go index 640b13a83..2da4d8f3a 100644 --- a/ipn/store/kubestore/store_kube.go +++ b/ipn/store/kubestore/store_kube.go @@ -7,6 +7,7 @@ package kubestore import ( "context" + "fmt" "net" "strings" "time" @@ -100,6 +101,19 @@ func (s *Store) WriteState(id ipn.StateKey, bs []byte) error { return err } 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{ { 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 { - return err + return fmt.Errorf("error patching Secret %s with /data/%s field", s.secretName, sanitizeKey(id)) } return nil } diff --git a/kube/client.go b/kube/client.go index 15130503f..62daa366e 100644 --- a/kube/client.go +++ b/kube/client.go @@ -240,7 +240,7 @@ func (c *client) UpdateSecret(ctx context.Context, s *Secret) error { } // 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 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 // 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 // so that the pod tries to fail alive if the permissions exist and there's just