summaryrefslogtreecommitdiffhomepage
path: root/cmd/k8s-operator/proxy.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/k8s-operator/proxy.go')
-rw-r--r--cmd/k8s-operator/proxy.go81
1 files changed, 56 insertions, 25 deletions
diff --git a/cmd/k8s-operator/proxy.go b/cmd/k8s-operator/proxy.go
index 258a958fa..45b048f6f 100644
--- a/cmd/k8s-operator/proxy.go
+++ b/cmd/k8s-operator/proxy.go
@@ -22,6 +22,7 @@ import (
"k8s.io/client-go/transport"
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
+ sessionrecording "tailscale.com/k8s-operator/session-recording"
tskube "tailscale.com/kube"
"tailscale.com/ssh/tailssh"
"tailscale.com/tailcfg"
@@ -36,12 +37,6 @@ var whoIsKey = ctxkey.New("", (*apitype.WhoIsResponse)(nil))
var (
// counterNumRequestsproxies counts the number of API server requests proxied via this proxy.
counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
-
- // counterSessionRecordingsAttempted counts the number of session recording attempts.
- counterSessionRecordingsAttempted = clientmetric.NewCounter("k8s_auth_proxy__session_recordings_attempted")
-
- // counterSessionRecordingsUploaded counts the number of successfully uploaded session recordings.
- counterSessionRecordingsUploaded = clientmetric.NewCounter("k8s_auth_proxy_session_recordings_uploaded")
)
type apiServerProxyMode int
@@ -173,7 +168,9 @@ func runAPIServerProxy(ts *tsnet.Server, rt http.RoundTripper, log *zap.SugaredL
mux := http.NewServeMux()
mux.HandleFunc("/", ap.serveDefault)
- mux.HandleFunc("/api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExec)
+ mux.HandleFunc("POST /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecSPDY)
+
+ mux.HandleFunc("GET /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecWS)
hs := &http.Server{
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
@@ -214,9 +211,10 @@ func (ap *apiserverProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
}
-// serveExec serves 'kubectl exec' requests, optionally configuring the kubectl
-// exec sessions to be recorded.
-func (ap *apiserverProxy) serveExec(w http.ResponseWriter, r *http.Request) {
+// serveExecWS serves 'kubectl exec' requests, optionally configuring the
+// kubectl exec sessions to be recorded. It should only be called for requests
+// for sessions that use WebSockets protocol for streaming.
+func (ap *apiserverProxy) serveExecWS(w http.ResponseWriter, r *http.Request) {
who, err := ap.whoIs(r)
if err != nil {
ap.authError(w, err)
@@ -232,14 +230,59 @@ func (ap *apiserverProxy) serveExec(w http.ResponseWriter, r *http.Request) {
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
return
}
- counterSessionRecordingsAttempted.Add(1) // at this point we know that users intended for this session to be recorded
+ sessionrecording.CounterSessionRecordingsAttempted.Add(1) // at this point we know that users intended for this session to be recorded
if !failOpen && len(addrs) == 0 {
msg := "forbidden: 'kubectl exec' session must be recorded, but no recorders are available."
ap.log.Error(msg)
http.Error(w, msg, http.StatusForbidden)
return
}
- if r.Method != "POST" || r.Header.Get("Upgrade") != "SPDY/3.1" {
+ if h := r.Header.Get("Upgrade"); h != "websocket" {
+ msg := fmt.Sprintf("[unexpected] 'kubectl exec' session was initiated for WebSocket protocol, but the request does not contain expected upgrade header, wants: 'websocket', got: %q", h)
+ if failOpen {
+ msg = msg + "; failure mode is 'fail open'; continuing session without recording."
+ ap.log.Warn(msg)
+ ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
+ return
+ }
+ ap.log.Error(msg)
+ msg += "; failure mode is 'fail closed'; closing connection."
+ http.Error(w, msg, 403)
+ return
+ } else {
+ ap.log.Debugf("detected 'kubectl exec' session streaming protocol is WebSockets")
+ }
+ wsH := sessionrecording.New(ap.ts, r, who, w, r.PathValue("pod"), r.PathValue("namespace"), sessionrecording.WebSocketsProtocol, addrs, failOpen, tailssh.ConnectToRecorder, ap.log)
+
+ ap.rp.ServeHTTP(wsH, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
+}
+
+// serveExecSPDY serves 'kubectl exec' requests, optionally configuring the
+// kubectl exec sessions to be recorded. It should only be called for requests
+// that initate 'kubectl exec' sessions using the SPDY protocol for streaming.
+func (ap *apiserverProxy) serveExecSPDY(w http.ResponseWriter, r *http.Request) {
+ who, err := ap.whoIs(r)
+ if err != nil {
+ ap.authError(w, err)
+ return
+ }
+ counterNumRequestsProxied.Add(1)
+ failOpen, addrs, err := determineRecorderConfig(who)
+ if err != nil {
+ ap.log.Errorf("error trying to determine whether the 'kubectl exec' session needs to be recorded: %v", err)
+ return
+ }
+ if failOpen && len(addrs) == 0 { // will not record
+ ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
+ return
+ }
+ if !failOpen && len(addrs) == 0 {
+ msg := "forbidden: 'kubectl exec' session must be recorded, but no recorders are available."
+ ap.log.Error(msg)
+ http.Error(w, msg, 403)
+ return
+ }
+ if r.Header.Get("Upgrade") != "SPDY/3.1" {
msg := "'kubectl exec' session recording is configured, but the request is not over SPDY. Session recording is currently only supported for SPDY based clients"
if failOpen {
msg = msg + "; failure mode is 'fail open'; continuing session without recording."
@@ -252,19 +295,7 @@ func (ap *apiserverProxy) serveExec(w http.ResponseWriter, r *http.Request) {
http.Error(w, msg, http.StatusForbidden)
return
}
- spdyH := &spdyHijacker{
- ts: ap.ts,
- req: r,
- who: who,
- ResponseWriter: w,
- log: ap.log,
- pod: r.PathValue("pod"),
- ns: r.PathValue("namespace"),
- addrs: addrs,
- failOpen: failOpen,
- connectToRecorder: tailssh.ConnectToRecorder,
- }
-
+ spdyH := sessionrecording.New(ap.ts, r, who, w, r.PathValue("pod"), r.PathValue("namespace"), sessionrecording.SPDYProtocol, addrs, failOpen, tailssh.ConnectToRecorder, ap.log)
ap.rp.ServeHTTP(spdyH, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
}