|
|
|
|
@ -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 |
|
|
|
|
} |
|
|
|
|
|