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