cmd/containerboot: gracefully degrade if missing patch permissions in k8s.
Fixes #6629. Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
committed by
Dave Anderson
parent
a887ca7efe
commit
367228ef82
+71
-43
@@ -21,54 +21,82 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// 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)
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// something wrong with SelfSubjectAccessReviews. There shouldn't be, pods
|
||||
// should always be able to use SSARs to assess their own permissions, but since
|
||||
// we didn't use to check permissions this way we'll be cautious in case some
|
||||
// old version of k8s deviates from the current behavior.
|
||||
func checkSecretPermissions(ctx context.Context, secretName string) (canPatch bool, err error) {
|
||||
var errs []error
|
||||
for _, verb := range []string{"get", "update"} {
|
||||
ok, err := checkPermission(ctx, verb, secretName)
|
||||
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)
|
||||
log.Printf("error checking %s permission on secret %s: %v", verb, secretName, err)
|
||||
} else if !ok {
|
||||
errs = append(errs, fmt.Errorf("missing %s permission on secret %q", verb, secretName))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
if len(errs) > 0 {
|
||||
return false, multierr.New(errs...)
|
||||
}
|
||||
ok, err := checkPermission(ctx, "patch", secretName)
|
||||
if err != nil {
|
||||
log.Printf("error checking patch permission on secret %s: %v", secretName, err)
|
||||
return false, nil
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// checkPermission reports whether the current pod has permission to use the
|
||||
// given verb (e.g. get, update, patch) on secretName.
|
||||
func checkPermission(ctx context.Context, verb, secretName string) (bool, error) {
|
||||
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 false, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", bytes.NewReader(bs))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
resp, err := doKubeRequest(ctx, req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var res struct {
|
||||
Status struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
} `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(bs, &res); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.Status.Allowed, nil
|
||||
}
|
||||
|
||||
// findKeyInKubeSecret inspects the kube secret secretName for a data
|
||||
|
||||
Reference in New Issue
Block a user