@@ -16,12 +16,26 @@ import (
1616
1717 "tailscale.com/client/tailscale"
1818 "tailscale.com/client/tailscale/apitype"
19+ "tailscale.com/tailcfg"
1920 "tailscale.com/tsnet"
2021 "tailscale.com/types/logger"
22+ "tailscale.com/util/set"
2123)
2224
2325type whoIsKey struct {}
2426
27+ // whoIsFromRequest returns the WhoIsResponse previously stashed by a call to
28+ // addWhoIsToRequest.
29+ func whoIsFromRequest (r * http.Request ) * apitype.WhoIsResponse {
30+ return r .Context ().Value (whoIsKey {}).(* apitype.WhoIsResponse )
31+ }
32+
33+ // addWhoIsToRequest stashes who in r's context, retrievable by a call to
34+ // whoIsFromRequest.
35+ func addWhoIsToRequest (r * http.Request , who * apitype.WhoIsResponse ) * http.Request {
36+ return r .WithContext (context .WithValue (r .Context (), whoIsKey {}, who ))
37+ }
38+
2539// authProxy is an http.Handler that authenticates requests using the Tailscale
2640// LocalAPI and then proxies them to the Kubernetes API.
2741type authProxy struct {
@@ -37,8 +51,7 @@ func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
3751 http .Error (w , "failed to authenticate caller" , http .StatusInternalServerError )
3852 return
3953 }
40- r = r .WithContext (context .WithValue (r .Context (), whoIsKey {}, who ))
41- h .rp .ServeHTTP (w , r )
54+ h .rp .ServeHTTP (w , addWhoIsToRequest (r , who ))
4255}
4356
4457// runAuthProxy runs an HTTP server that authenticates requests using the
@@ -67,6 +80,10 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
6780 lc : lc ,
6881 rp : & httputil.ReverseProxy {
6982 Director : func (r * http.Request ) {
83+ // Replace the URL with the Kubernetes APIServer.
84+ r .URL .Scheme = u .Scheme
85+ r .URL .Host = u .Host
86+
7087 // We want to proxy to the Kubernetes API, but we want to use
7188 // the caller's identity to do so. We do this by impersonating
7289 // the caller using the Kubernetes User Impersonation feature:
@@ -85,21 +102,9 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
85102 }
86103
87104 // Now add the impersonation headers that we want.
88- who := r .Context ().Value (whoIsKey {}).(* apitype.WhoIsResponse )
89- if who .Node .IsTagged () {
90- // Use the nodes FQDN as the username, and the nodes tags as the groups.
91- // "Impersonate-Group" requires "Impersonate-User" to be set.
92- r .Header .Set ("Impersonate-User" , strings .TrimSuffix (who .Node .Name , "." ))
93- for _ , tag := range who .Node .Tags {
94- r .Header .Add ("Impersonate-Group" , tag )
95- }
96- } else {
97- r .Header .Set ("Impersonate-User" , who .UserProfile .LoginName )
105+ if err := addImpersonationHeaders (r ); err != nil {
106+ panic ("failed to add impersonation headers: " + err .Error ())
98107 }
99-
100- // Replace the URL with the Kubernetes APIServer.
101- r .URL .Scheme = u .Scheme
102- r .URL .Host = u .Host
103108 },
104109 Transport : rt ,
105110 },
@@ -118,3 +123,58 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
118123 log .Fatalf ("runAuthProxy: failed to serve %v" , err )
119124 }
120125}
126+
127+ const capabilityName = "https://tailscale.com/cap/kubernetes"
128+
129+ type capRule struct {
130+ // Impersonate is a list of rules that specify how to impersonate the caller
131+ // when proxying to the Kubernetes API.
132+ Impersonate * impersonateRule `json:"impersonate,omitempty"`
133+ }
134+
135+ // TODO(maisem): move this to some well-known location so that it can be shared
136+ // with control.
137+ type impersonateRule struct {
138+ Groups []string `json:"groups,omitempty"`
139+ }
140+
141+ // addImpersonationHeaders adds the appropriate headers to r to impersonate the
142+ // caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
143+ // in the context by the authProxy.
144+ func addImpersonationHeaders (r * http.Request ) error {
145+ who := whoIsFromRequest (r )
146+ rules , err := tailcfg .UnmarshalCapJSON [capRule ](who .CapMap , capabilityName )
147+ if err != nil {
148+ return fmt .Errorf ("failed to unmarshal capability: %v" , err )
149+ }
150+
151+ var groupsAdded set.Slice [string ]
152+ for _ , rule := range rules {
153+ if rule .Impersonate == nil {
154+ continue
155+ }
156+ for _ , group := range rule .Impersonate .Groups {
157+ if groupsAdded .Contains (group ) {
158+ continue
159+ }
160+ r .Header .Add ("Impersonate-Group" , group )
161+ groupsAdded .Add (group )
162+ }
163+ }
164+
165+ if ! who .Node .IsTagged () {
166+ r .Header .Set ("Impersonate-User" , who .UserProfile .LoginName )
167+ return nil
168+ }
169+ // "Impersonate-Group" requires "Impersonate-User" to be set, so we set it
170+ // to the node FQDN for tagged nodes.
171+ r .Header .Set ("Impersonate-User" , strings .TrimSuffix (who .Node .Name , "." ))
172+
173+ // For legacy behavior (before caps), set the groups to the nodes tags.
174+ if groupsAdded .Slice ().Len () == 0 {
175+ for _ , tag := range who .Node .Tags {
176+ r .Header .Add ("Impersonate-Group" , tag )
177+ }
178+ }
179+ return nil
180+ }
0 commit comments