diff --git a/cmd/containerboot/kube.go b/cmd/containerboot/kube.go index 04b988f34..5a0d6d6a4 100644 --- a/cmd/containerboot/kube.go +++ b/cmd/containerboot/kube.go @@ -23,6 +23,54 @@ import ( "time" ) +// checkSecretPermissions checks that the current pod has permission to read, +// write and patch secretName in the cluster. +func checkSecretPermissions(ctx context.Context, secretName string) error { + for _, verb := range []string{"get", "update", "patch"} { + sar := map[string]any{ + "apiVersion": "authorization.k8s.io/v1", + "kind": "SelfSubjectAccessReview", + "spec": map[string]any{ + "resourceAttributes": map[string]any{ + "namespace": kubeNamespace, + "verb": verb, + "resource": "secrets", + "name": secretName, + }, + }, + } + bs, err := json.Marshal(sar) + if err != nil { + return err + } + req, err := http.NewRequest("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", bytes.NewReader(bs)) + if err != nil { + return err + } + resp, err := doKubeRequest(ctx, req) + if err != nil { + return err + } + defer resp.Body.Close() + bs, err = io.ReadAll(resp.Body) + if err != nil { + return err + } + var res struct { + Status struct { + Allowed bool `json:"allowed"` + } `json:"status"` + } + if err := json.Unmarshal(bs, &res); err != nil { + return err + } + if !res.Status.Allowed { + return fmt.Errorf("missing permission: cannot %s secret %q", verb, secretName) + } + } + return nil +} + // findKeyInKubeSecret inspects the kube secret secretName for a data // field called "authkey", and returns its value if present. func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) { @@ -193,8 +241,8 @@ func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error) if err != nil { return nil, err } - if resp.StatusCode != http.StatusOK { - return resp, fmt.Errorf("got non-200 status code %d", resp.StatusCode) + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return resp, fmt.Errorf("got non-200/201 status code %d", resp.StatusCode) } return resp, nil } diff --git a/cmd/containerboot/main.go b/cmd/containerboot/main.go index 28f952b1b..583de3e3a 100644 --- a/cmd/containerboot/main.go +++ b/cmd/containerboot/main.go @@ -116,16 +116,21 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.AuthKey == "" { - key, err := findKeyInKubeSecret(ctx, cfg.KubeSecret) - if err != nil { - log.Fatalf("Getting authkey from kube secret: %v", err) + if cfg.InKubernetes && cfg.KubeSecret != "" { + if err := checkSecretPermissions(ctx, cfg.KubeSecret); err != nil { + log.Fatalf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err) } - if key != "" { - log.Print("Using authkey found in kube secret") - cfg.AuthKey = key - } else { - log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.") + if cfg.AuthKey == "" { + key, err := findKeyInKubeSecret(ctx, cfg.KubeSecret) + if err != nil { + log.Fatalf("Getting authkey from kube secret: %v", err) + } + if key != "" { + log.Print("Using authkey found in kube secret") + cfg.AuthKey = key + } else { + log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.") + } } } diff --git a/cmd/containerboot/main_test.go b/cmd/containerboot/main_test.go index 4b5bfd043..95a616e32 100644 --- a/cmd/containerboot/main_test.go +++ b/cmd/containerboot/main_test.go @@ -784,6 +784,12 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer bearer_token" { panic("client didn't provide bearer token in request") } + if r.URL.Path == "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews" { + // Just say yes to all SARs, we don't enforce RBAC. + w.Header().Set("Content-Type", "application/json") + fmt.Fprintln(w, `{"status":{"allowed":true}}`) + return + } if r.URL.Path != "/api/v1/namespaces/default/secrets/tailscale" { panic(fmt.Sprintf("unhandled fake kube api path %q", r.URL.Path)) }