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
@ -114,10 +114,11 @@ func TestContainerBoot(t *testing.T) {
|
||||
WantFiles map[string]string
|
||||
}
|
||||
tests := []struct {
|
||||
Name string
|
||||
Env map[string]string
|
||||
KubeSecret map[string]string
|
||||
Phases []phase
|
||||
Name string
|
||||
Env map[string]string
|
||||
KubeSecret map[string]string
|
||||
KubeDenyPatch bool
|
||||
Phases []phase
|
||||
}{
|
||||
{
|
||||
// Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
|
||||
@ -370,6 +371,35 @@ func TestContainerBoot(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "kube_storage_no_patch",
|
||||
Env: map[string]string{
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
},
|
||||
KubeSecret: map[string]string{},
|
||||
KubeDenyPatch: true,
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
{
|
||||
Status: ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
TailscaleIPs: tsIPs,
|
||||
Self: &ipnstate.PeerStatus{
|
||||
ID: tailcfg.StableNodeID("myID"),
|
||||
},
|
||||
},
|
||||
WantKubeSecret: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Same as previous, but deletes the authkey from the kube secret.
|
||||
Name: "kube_storage_auth_once",
|
||||
@ -492,6 +522,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
for k, v := range test.KubeSecret {
|
||||
kube.SetSecret(k, v)
|
||||
}
|
||||
kube.SetPatching(!test.KubeDenyPatch)
|
||||
|
||||
cmd := exec.Command(boot)
|
||||
cmd.Env = []string{
|
||||
@ -722,7 +753,8 @@ type kubeServer struct {
|
||||
srv *httptest.Server
|
||||
|
||||
sync.Mutex
|
||||
secret map[string]string
|
||||
secret map[string]string
|
||||
canPatch bool
|
||||
}
|
||||
|
||||
func (k *kubeServer) Secret() map[string]string {
|
||||
@ -741,6 +773,12 @@ func (k *kubeServer) SetSecret(key, val string) {
|
||||
k.secret[key] = val
|
||||
}
|
||||
|
||||
func (k *kubeServer) SetPatching(canPatch bool) {
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
k.canPatch = canPatch
|
||||
}
|
||||
|
||||
func (k *kubeServer) Reset() {
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
@ -784,16 +822,39 @@ 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" {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/namespaces/default/secrets/tailscale":
|
||||
k.serveSecret(w, r)
|
||||
case "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews":
|
||||
k.serveSSAR(w, r)
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled fake kube api path %q", r.URL.Path))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kubeServer) serveSSAR(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Spec struct {
|
||||
ResourceAttributes struct {
|
||||
Verb string `json:"verb"`
|
||||
} `json:"resourceAttributes"`
|
||||
} `json:"spec"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
panic(fmt.Sprintf("decoding SSAR request: %v", err))
|
||||
}
|
||||
ok := true
|
||||
if req.Spec.ResourceAttributes.Verb == "patch" {
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
ok = k.canPatch
|
||||
}
|
||||
// Just say yes to all SARs, we don't enforce RBAC.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"status":{"allowed":%v}}`, ok)
|
||||
}
|
||||
|
||||
func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusInternalServerError)
|
||||
@ -819,6 +880,11 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
panic("encode failed")
|
||||
}
|
||||
case "PATCH":
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
if !k.canPatch {
|
||||
panic("containerboot tried to patch despite not being allowed")
|
||||
}
|
||||
switch r.Header.Get("Content-Type") {
|
||||
case "application/json-patch+json":
|
||||
req := []struct {
|
||||
@ -828,8 +894,6 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.Unmarshal(bs, &req); err != nil {
|
||||
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
|
||||
}
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
for _, op := range req {
|
||||
if op.Op != "remove" {
|
||||
panic(fmt.Sprintf("unsupported json-patch op %q", op.Op))
|
||||
@ -846,8 +910,6 @@ func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.Unmarshal(bs, &req); err != nil {
|
||||
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
|
||||
}
|
||||
k.Lock()
|
||||
defer k.Unlock()
|
||||
for key, val := range req.Data {
|
||||
k.secret[key] = val
|
||||
}
|
||||
|
Reference in New Issue
Block a user