|
|
@ -23,7 +23,6 @@ import (
|
|
|
|
"tailscale.com/client/tailscale/apitype"
|
|
|
|
"tailscale.com/client/tailscale/apitype"
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
"tailscale.com/tsnet"
|
|
|
|
"tailscale.com/tsnet"
|
|
|
|
"tailscale.com/types/logger"
|
|
|
|
|
|
|
|
"tailscale.com/util/clientmetric"
|
|
|
|
"tailscale.com/util/clientmetric"
|
|
|
|
"tailscale.com/util/set"
|
|
|
|
"tailscale.com/util/set"
|
|
|
|
)
|
|
|
|
)
|
|
|
@ -109,21 +108,21 @@ func maybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config,
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
|
|
|
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy").Infof, mode)
|
|
|
|
go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy"), mode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// apiserverProxy is an http.Handler that authenticates requests using the Tailscale
|
|
|
|
// apiserverProxy is an http.Handler that authenticates requests using the Tailscale
|
|
|
|
// LocalAPI and then proxies them to the Kubernetes API.
|
|
|
|
// LocalAPI and then proxies them to the Kubernetes API.
|
|
|
|
type apiserverProxy struct {
|
|
|
|
type apiserverProxy struct {
|
|
|
|
logf logger.Logf
|
|
|
|
log *zap.SugaredLogger
|
|
|
|
lc *tailscale.LocalClient
|
|
|
|
lc *tailscale.LocalClient
|
|
|
|
rp *httputil.ReverseProxy
|
|
|
|
rp *httputil.ReverseProxy
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr)
|
|
|
|
who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
h.logf("failed to authenticate caller: %v", err)
|
|
|
|
h.log.Errorf("failed to authenticate caller: %v", err)
|
|
|
|
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
|
|
|
|
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -145,7 +144,7 @@ func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// are passed through to the Kubernetes API.
|
|
|
|
// are passed through to the Kubernetes API.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// It never returns.
|
|
|
|
// It never returns.
|
|
|
|
func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf, mode apiServerProxyMode) {
|
|
|
|
func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLogger, mode apiServerProxyMode) {
|
|
|
|
if mode == apiserverProxyModeDisabled {
|
|
|
|
if mode == apiserverProxyModeDisabled {
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -163,8 +162,8 @@ func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf,
|
|
|
|
log.Fatalf("could not get local client: %v", err)
|
|
|
|
log.Fatalf("could not get local client: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ap := &apiserverProxy{
|
|
|
|
ap := &apiserverProxy{
|
|
|
|
logf: logf,
|
|
|
|
log: log,
|
|
|
|
lc: lc,
|
|
|
|
lc: lc,
|
|
|
|
rp: &httputil.ReverseProxy{
|
|
|
|
rp: &httputil.ReverseProxy{
|
|
|
|
Rewrite: func(r *httputil.ProxyRequest) {
|
|
|
|
Rewrite: func(r *httputil.ProxyRequest) {
|
|
|
|
// Replace the URL with the Kubernetes APIServer.
|
|
|
|
// Replace the URL with the Kubernetes APIServer.
|
|
|
@ -196,7 +195,7 @@ func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Now add the impersonation headers that we want.
|
|
|
|
// Now add the impersonation headers that we want.
|
|
|
|
if err := addImpersonationHeaders(r.Out); err != nil {
|
|
|
|
if err := addImpersonationHeaders(r.Out, log); err != nil {
|
|
|
|
panic("failed to add impersonation headers: " + err.Error())
|
|
|
|
panic("failed to add impersonation headers: " + err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
@ -213,6 +212,7 @@ func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf,
|
|
|
|
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
|
|
|
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
|
|
|
Handler: ap,
|
|
|
|
Handler: ap,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Infof("listening on %s", ln.Addr())
|
|
|
|
if err := hs.ServeTLS(ln, "", ""); err != nil {
|
|
|
|
if err := hs.ServeTLS(ln, "", ""); err != nil {
|
|
|
|
log.Fatalf("runAPIServerProxy: failed to serve %v", err)
|
|
|
|
log.Fatalf("runAPIServerProxy: failed to serve %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -235,7 +235,8 @@ type impersonateRule struct {
|
|
|
|
// addImpersonationHeaders adds the appropriate headers to r to impersonate the
|
|
|
|
// addImpersonationHeaders adds the appropriate headers to r to impersonate the
|
|
|
|
// caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
|
|
|
|
// caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
|
|
|
|
// in the context by the apiserverProxy.
|
|
|
|
// in the context by the apiserverProxy.
|
|
|
|
func addImpersonationHeaders(r *http.Request) error {
|
|
|
|
func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
|
|
|
|
|
|
|
|
log = log.With("remote", r.RemoteAddr)
|
|
|
|
who := whoIsFromRequest(r)
|
|
|
|
who := whoIsFromRequest(r)
|
|
|
|
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName)
|
|
|
|
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
@ -253,21 +254,26 @@ func addImpersonationHeaders(r *http.Request) error {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
r.Header.Add("Impersonate-Group", group)
|
|
|
|
r.Header.Add("Impersonate-Group", group)
|
|
|
|
groupsAdded.Add(group)
|
|
|
|
groupsAdded.Add(group)
|
|
|
|
|
|
|
|
log.Debugf("adding group impersonation header for user group %s", group)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !who.Node.IsTagged() {
|
|
|
|
if !who.Node.IsTagged() {
|
|
|
|
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
|
|
|
|
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
|
|
|
|
|
|
|
|
log.Debugf("adding user impersonation header for user %s", who.UserProfile.LoginName)
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// "Impersonate-Group" requires "Impersonate-User" to be set, so we set it
|
|
|
|
// "Impersonate-Group" requires "Impersonate-User" to be set, so we set it
|
|
|
|
// to the node FQDN for tagged nodes.
|
|
|
|
// to the node FQDN for tagged nodes.
|
|
|
|
r.Header.Set("Impersonate-User", strings.TrimSuffix(who.Node.Name, "."))
|
|
|
|
nodeName := strings.TrimSuffix(who.Node.Name, ".")
|
|
|
|
|
|
|
|
r.Header.Set("Impersonate-User", nodeName)
|
|
|
|
|
|
|
|
log.Debugf("adding user impersonation header for node name %s", nodeName)
|
|
|
|
|
|
|
|
|
|
|
|
// For legacy behavior (before caps), set the groups to the nodes tags.
|
|
|
|
// For legacy behavior (before caps), set the groups to the nodes tags.
|
|
|
|
if groupsAdded.Slice().Len() == 0 {
|
|
|
|
if groupsAdded.Slice().Len() == 0 {
|
|
|
|
for _, tag := range who.Node.Tags {
|
|
|
|
for _, tag := range who.Node.Tags {
|
|
|
|
r.Header.Add("Impersonate-Group", tag)
|
|
|
|
r.Header.Add("Impersonate-Group", tag)
|
|
|
|
|
|
|
|
log.Debugf("adding group impersonation header for node tag %s", tag)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|