2021-01-20 00:37:02 +00:00
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package impersonator
import (
"fmt"
2021-03-10 18:30:06 +00:00
"net"
2021-01-20 00:37:02 +00:00
"net/http"
"net/http/httputil"
"net/url"
"strings"
2021-02-23 01:23:11 +00:00
"time"
2021-01-20 00:37:02 +00:00
2021-03-14 01:25:23 +00:00
apierrors "k8s.io/apimachinery/pkg/api/errors"
2021-03-10 18:30:06 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2021-02-15 23:00:10 +00:00
"k8s.io/apimachinery/pkg/runtime"
2021-03-14 01:25:23 +00:00
"k8s.io/apimachinery/pkg/runtime/schema"
2021-03-10 18:30:06 +00:00
"k8s.io/apimachinery/pkg/runtime/serializer"
2021-03-11 21:20:25 +00:00
"k8s.io/apimachinery/pkg/util/errors"
2021-03-10 18:30:06 +00:00
"k8s.io/apimachinery/pkg/util/sets"
2021-03-12 15:33:30 +00:00
"k8s.io/apiserver/pkg/authentication/user"
2021-03-10 18:30:06 +00:00
"k8s.io/apiserver/pkg/authorization/authorizer"
2021-03-14 01:25:23 +00:00
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
2021-03-10 18:30:06 +00:00
"k8s.io/apiserver/pkg/endpoints/request"
2021-03-14 01:25:23 +00:00
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
2021-03-10 18:30:06 +00:00
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
2021-03-12 21:36:37 +00:00
"k8s.io/apiserver/pkg/server/filters"
2021-03-10 18:30:06 +00:00
genericoptions "k8s.io/apiserver/pkg/server/options"
2021-01-20 00:37:02 +00:00
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
2021-02-18 15:13:24 +00:00
"go.pinniped.dev/internal/constable"
2021-03-11 21:20:25 +00:00
"go.pinniped.dev/internal/dynamiccert"
2021-03-10 18:30:06 +00:00
"go.pinniped.dev/internal/httputil/securityheader"
2021-02-09 20:28:56 +00:00
"go.pinniped.dev/internal/kubeclient"
2021-03-10 18:30:06 +00:00
"go.pinniped.dev/internal/plog"
2021-01-20 00:37:02 +00:00
)
2021-03-10 18:30:06 +00:00
// FactoryFunc is a function which can create an impersonator server.
// It returns a function which will start the impersonator server.
// That start function takes a stopCh which can be used to stop the server.
// Once a server has been stopped, don't start it again using the start function.
// Instead, call the factory function again to get a new start function.
type FactoryFunc func (
port int ,
2021-03-11 21:20:25 +00:00
dynamicCertProvider dynamiccert . Private ,
impersonationProxySignerCA dynamiccert . Public ,
2021-03-10 18:30:06 +00:00
) ( func ( stopCh <- chan struct { } ) error , error )
func New (
port int ,
2021-03-11 21:20:25 +00:00
dynamicCertProvider dynamiccert . Private ,
impersonationProxySignerCA dynamiccert . Public ,
2021-03-10 18:30:06 +00:00
) ( func ( stopCh <- chan struct { } ) error , error ) {
return newInternal ( port , dynamicCertProvider , impersonationProxySignerCA , nil , nil )
2021-01-20 00:37:02 +00:00
}
2021-03-10 18:30:06 +00:00
func newInternal ( //nolint:funlen // yeah, it's kind of long.
port int ,
2021-03-11 21:20:25 +00:00
dynamicCertProvider dynamiccert . Private ,
impersonationProxySignerCA dynamiccert . Public ,
2021-03-10 18:30:06 +00:00
clientOpts [ ] kubeclient . Option , // for unit testing, should always be nil in production
recOpts func ( * genericoptions . RecommendedOptions ) , // for unit testing, should always be nil in production
) ( func ( stopCh <- chan struct { } ) error , error ) {
var listener net . Listener
constructServer := func ( ) ( func ( stopCh <- chan struct { } ) error , error ) {
2021-03-11 20:52:39 +00:00
// Bare minimum server side scheme to allow for status messages to be encoded.
2021-03-10 18:30:06 +00:00
scheme := runtime . NewScheme ( )
metav1 . AddToGroupVersion ( scheme , metav1 . Unversioned )
codecs := serializer . NewCodecFactory ( scheme )
2021-03-11 20:52:39 +00:00
// This is unused for now but it is a safe value that we could use in the future.
2021-03-10 18:30:06 +00:00
defaultEtcdPathPrefix := "/pinniped-impersonation-proxy-registry"
recommendedOptions := genericoptions . NewRecommendedOptions (
defaultEtcdPathPrefix ,
codecs . LegacyCodec ( ) ,
)
recommendedOptions . Etcd = nil // turn off etcd storage because we don't need it yet
recommendedOptions . SecureServing . ServerCert . GeneratedCert = dynamicCertProvider // serving certs (end user facing)
recommendedOptions . SecureServing . BindPort = port
2021-03-11 20:52:39 +00:00
// Wire up the impersonation proxy signer CA as another valid authenticator for client cert auth,
// along with the Kube API server's CA.
2021-03-12 15:33:30 +00:00
// Note: any changes to the the Authentication stack need to be kept in sync with any assumptions made
// by getTransportForUser, especially if we ever update the TCR API to start returning bearer tokens.
2021-03-10 18:30:06 +00:00
kubeClient , err := kubeclient . New ( clientOpts ... )
2021-02-09 20:28:56 +00:00
if err != nil {
return nil , err
}
2021-03-11 20:52:39 +00:00
kubeClientCA , err := dynamiccertificates . NewDynamicCAFromConfigMapController (
"client-ca" , metav1 . NamespaceSystem , "extension-apiserver-authentication" , "client-ca-file" , kubeClient . Kubernetes ,
)
2021-03-10 18:30:06 +00:00
if err != nil {
return nil , err
}
recommendedOptions . Authentication . ClientCert . ClientCA = "---irrelevant-but-needs-to-be-non-empty---" // drop when we pick up https://github.com/kubernetes/kubernetes/pull/100055
2021-03-11 20:52:39 +00:00
recommendedOptions . Authentication . ClientCert . CAContentProvider = dynamiccertificates . NewUnionCAContentProvider (
impersonationProxySignerCA , kubeClientCA ,
)
2021-03-10 18:30:06 +00:00
if recOpts != nil {
recOpts ( recommendedOptions )
}
serverConfig := genericapiserver . NewRecommendedConfig ( codecs )
// Note that ApplyTo is going to create a network listener and bind to the requested port.
// It puts this listener into serverConfig.SecureServing.Listener.
err = recommendedOptions . ApplyTo ( serverConfig )
if serverConfig . SecureServing != nil {
// Set the pointer from the outer function to allow the outer function to close the listener in case
// this function returns an error for any reason anywhere below here.
listener = serverConfig . SecureServing . Listener
}
if err != nil {
return nil , err
}
2021-03-11 20:52:39 +00:00
// Loopback authentication to this server does not really make sense since we just proxy everything to
// the Kube API server, thus we replace loopback connection config with one that does direct connections
// the Kube API server. Loopback config is mainly used by post start hooks, so this is mostly future proofing.
2021-03-10 18:30:06 +00:00
serverConfig . LoopbackClientConfig = rest . CopyConfig ( kubeClient . ProtoConfig ) // assume proto is safe (hooks can override)
2021-03-11 20:52:39 +00:00
// Remove the bearer token so our authorizer does not get stomped on by AuthorizeClientBearerToken.
// See sanity checks at the end of this function.
2021-03-10 18:30:06 +00:00
serverConfig . LoopbackClientConfig . BearerToken = ""
2021-03-12 21:36:37 +00:00
// match KAS exactly since our long running operations are just a proxy to it
2021-03-15 13:43:06 +00:00
// this must be kept in sync with github.com/kubernetes/kubernetes/cmd/kube-apiserver/app/server.go
// this is nothing to stress about - it has not changed since the beginning of Kube:
// v1.6 no-op move away from regex to request info https://github.com/kubernetes/kubernetes/pull/38119
// v1.1 added pods/attach to the list https://github.com/kubernetes/kubernetes/pull/13705
2021-03-12 21:36:37 +00:00
serverConfig . LongRunningFunc = filters . BasicLongRunningRequestCheck (
sets . NewString ( "watch" , "proxy" ) ,
sets . NewString ( "attach" , "exec" , "proxy" , "log" , "portforward" ) ,
)
2021-03-11 20:52:39 +00:00
// Assume proto config is safe because transport level configs do not use rest.ContentConfig.
// Thus if we are interacting with actual APIs, they should be using pre-built clients.
2021-03-12 14:56:34 +00:00
impersonationProxyFunc , err := newImpersonationReverseProxyFunc ( rest . CopyConfig ( kubeClient . ProtoConfig ) )
2021-03-10 18:30:06 +00:00
if err != nil {
return nil , err
}
defaultBuildHandlerChainFunc := serverConfig . BuildHandlerChainFunc
serverConfig . BuildHandlerChainFunc = func ( _ http . Handler , c * genericapiserver . Config ) http . Handler {
2021-03-11 20:52:39 +00:00
// We ignore the passed in handler because we never have any REST APIs to delegate to.
2021-03-12 14:56:34 +00:00
handler := impersonationProxyFunc ( c )
handler = defaultBuildHandlerChainFunc ( handler , c )
2021-03-10 18:30:06 +00:00
handler = securityheader . Wrap ( handler )
return handler
}
2021-01-20 00:37:02 +00:00
2021-03-11 20:52:39 +00:00
// Overwrite the delegating authorizer with one that only cares about impersonation.
// Empty string is disallowed because request info has had bugs in the past where it would leave it empty.
2021-03-10 18:30:06 +00:00
disallowedVerbs := sets . NewString ( "" , "impersonate" )
noImpersonationAuthorizer := & comparableAuthorizer {
AuthorizerFunc : func ( a authorizer . Attributes ) ( authorizer . Decision , string , error ) {
2021-03-11 20:52:39 +00:00
// Supporting impersonation is not hard, it would just require a bunch of testing
// and configuring the audit layer (to preserve the caller) which we can do later.
// We would also want to delete the incoming impersonation headers
2021-03-10 18:30:06 +00:00
// instead of overwriting the delegating authorizer, we would
2021-03-11 20:52:39 +00:00
// actually use it to make the impersonation authorization checks.
2021-03-10 18:30:06 +00:00
if disallowedVerbs . Has ( a . GetVerb ( ) ) {
return authorizer . DecisionDeny , "impersonation is not allowed or invalid verb" , nil
}
return authorizer . DecisionAllow , "deferring authorization to kube API server" , nil
} ,
}
2021-03-11 20:52:39 +00:00
// Set our custom authorizer before calling Compete(), which will use it.
2021-03-10 18:30:06 +00:00
serverConfig . Authorization . Authorizer = noImpersonationAuthorizer
impersonationProxyServer , err := serverConfig . Complete ( ) . New ( "impersonation-proxy" , genericapiserver . NewEmptyDelegate ( ) )
if err != nil {
return nil , err
}
preparedRun := impersonationProxyServer . PrepareRun ( )
2021-03-11 20:52:39 +00:00
// Sanity check. Make sure that our custom authorizer is still in place and did not get changed or wrapped.
2021-03-10 18:30:06 +00:00
if preparedRun . Authorizer != noImpersonationAuthorizer {
return nil , constable . Error ( "invalid mutation of impersonation authorizer detected" )
}
2021-03-11 20:52:39 +00:00
// Sanity check. Assert that we have a functioning token file to use and no bearer token.
2021-03-10 18:30:06 +00:00
if len ( preparedRun . LoopbackClientConfig . BearerToken ) != 0 || len ( preparedRun . LoopbackClientConfig . BearerTokenFile ) == 0 {
return nil , constable . Error ( "invalid impersonator loopback rest config has wrong bearer token semantics" )
}
return preparedRun . Run , nil
}
result , err := constructServer ( )
// If there was any error during construction, then we would like to close the listener to free up the port.
2021-01-20 00:37:02 +00:00
if err != nil {
2021-03-10 18:30:06 +00:00
errs := [ ] error { err }
if listener != nil {
errs = append ( errs , listener . Close ( ) )
}
return nil , errors . NewAggregate ( errs )
2021-01-20 00:37:02 +00:00
}
2021-03-10 18:30:06 +00:00
return result , nil
}
// No-op wrapping around AuthorizerFunc to allow for comparisons.
type comparableAuthorizer struct {
authorizer . AuthorizerFunc
}
2021-01-20 00:37:02 +00:00
2021-03-12 14:56:34 +00:00
func newImpersonationReverseProxyFunc ( restConfig * rest . Config ) ( func ( * genericapiserver . Config ) http . Handler , error ) {
2021-03-10 18:30:06 +00:00
serverURL , err := url . Parse ( restConfig . Host )
2021-01-20 00:37:02 +00:00
if err != nil {
return nil , fmt . Errorf ( "could not parse host URL from in-cluster config: %w" , err )
}
2021-03-18 19:32:33 +00:00
http1RoundTripper , err := getTransportForProtocol ( restConfig , "http/1.1" )
2021-01-20 00:37:02 +00:00
if err != nil {
2021-03-18 19:32:33 +00:00
return nil , fmt . Errorf ( "could not get http/1.1 round tripper: %w" , err )
2021-01-20 00:37:02 +00:00
}
2021-03-18 19:32:33 +00:00
http2RoundTripper , err := getTransportForProtocol ( restConfig , "h2" )
2021-01-20 00:37:02 +00:00
if err != nil {
2021-03-18 19:32:33 +00:00
return nil , fmt . Errorf ( "could not get http/2.0 round tripper: %w" , err )
2021-01-20 00:37:02 +00:00
}
2021-03-12 14:56:34 +00:00
return func ( c * genericapiserver . Config ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if len ( r . Header . Values ( "Authorization" ) ) != 0 {
plog . Warning ( "aggregated API server logic did not delete authorization header but it is always supposed to do so" ,
"url" , r . URL . String ( ) ,
"method" , r . Method ,
)
2021-03-14 01:25:23 +00:00
newInternalErrResponse ( w , r , c . Serializer , "invalid authorization header" )
2021-03-12 14:56:34 +00:00
return
}
if err := ensureNoImpersonationHeaders ( r ) ; err != nil {
plog . Error ( "noImpersonationAuthorizer logic did not prevent nested impersonation but it is always supposed to do so" ,
err ,
"url" , r . URL . String ( ) ,
"method" , r . Method ,
)
2021-03-14 01:25:23 +00:00
newInternalErrResponse ( w , r , c . Serializer , "invalid impersonation" )
2021-03-12 14:56:34 +00:00
return
}
userInfo , ok := request . UserFrom ( r . Context ( ) )
if ! ok {
plog . Warning ( "aggregated API server logic did not set user info but it is always supposed to do so" ,
"url" , r . URL . String ( ) ,
"method" , r . Method ,
)
2021-03-14 01:25:23 +00:00
newInternalErrResponse ( w , r , c . Serializer , "invalid user" )
2021-03-12 14:56:34 +00:00
return
}
2021-03-18 19:32:33 +00:00
reqInfo , ok := request . RequestInfoFrom ( r . Context ( ) )
if ! ok {
plog . Warning ( "aggregated API server logic did not set request info but it is always supposed to do so" ,
"url" , r . URL . String ( ) ,
"method" , r . Method ,
)
newInternalErrResponse ( w , r , c . Serializer , "invalid request info" )
return
}
// when we are running regular requests (e.g., CRUD) we should always be able to use HTTP/2.0
// since KAS always supports that and it goes through proxies just fine. for long running
// requests (e.g., proxy, watch), we know they use http/1.1 with an upgrade to
// websockets/SPDY (this upgrade is NEVER to HTTP/2.0 as the KAS does not support that).
baseRT := http2RoundTripper
if c . LongRunningFunc ( r , reqInfo ) {
baseRT = http1RoundTripper
}
rt , err := getTransportForUser ( userInfo , baseRT )
2021-03-12 15:33:30 +00:00
if err != nil {
plog . WarningErr ( "rejecting request as we cannot act as the current user" , err ,
2021-03-12 14:56:34 +00:00
"url" , r . URL . String ( ) ,
"method" , r . Method ,
)
2021-03-14 01:25:23 +00:00
newInternalErrResponse ( w , r , c . Serializer , "unimplemented functionality - unable to act as current user" )
2021-03-12 14:56:34 +00:00
return
}
2021-03-15 23:11:45 +00:00
plog . Debug ( "impersonation proxy servicing request" , "method" , r . Method , "url" , r . URL . String ( ) )
2021-03-16 00:10:55 +00:00
plog . Trace ( "impersonation proxy servicing request was for user" , "method" , r . Method , "url" , r . URL . String ( ) ,
2021-03-12 14:56:34 +00:00
"username" , userInfo . GetName ( ) , // this info leak seems fine for trace level logs
2021-03-10 18:30:06 +00:00
)
2021-01-20 00:37:02 +00:00
2021-03-18 17:00:06 +00:00
// The proxy library used below will panic when the client disconnects abruptly, so in order to
// assure that this log message is always printed at the end of this func, it must be deferred.
defer plog . Debug ( "impersonation proxy finished servicing request" , "method" , r . Method , "url" , r . URL . String ( ) )
2021-03-12 14:56:34 +00:00
reverseProxy := httputil . NewSingleHostReverseProxy ( serverURL )
2021-03-12 15:33:30 +00:00
reverseProxy . Transport = rt
2021-03-12 14:56:34 +00:00
reverseProxy . FlushInterval = 200 * time . Millisecond // the "watch" verb will not work without this line
reverseProxy . ServeHTTP ( w , r )
} )
} , nil
2021-02-18 15:13:24 +00:00
}
2021-02-16 13:15:50 +00:00
func ensureNoImpersonationHeaders ( r * http . Request ) error {
2021-03-02 22:56:54 +00:00
for key := range r . Header {
2021-03-10 18:30:06 +00:00
if strings . HasPrefix ( key , "Impersonate" ) {
2021-03-02 22:56:54 +00:00
return fmt . Errorf ( "%q header already exists" , key )
2021-02-16 13:15:50 +00:00
}
}
2021-02-18 15:13:24 +00:00
2021-03-10 18:30:06 +00:00
return nil
2021-01-20 00:37:02 +00:00
}
2021-03-12 15:33:30 +00:00
func getTransportForUser ( userInfo user . Info , delegate http . RoundTripper ) ( http . RoundTripper , error ) {
if len ( userInfo . GetUID ( ) ) == 0 {
impersonateConfig := transport . ImpersonationConfig {
UserName : userInfo . GetName ( ) ,
Groups : userInfo . GetGroups ( ) ,
Extra : userInfo . GetExtra ( ) ,
}
// transport.NewImpersonatingRoundTripper clones the request before setting headers
// thus it will not accidentally mutate the input request (see http.Handler docs)
return transport . NewImpersonatingRoundTripper ( impersonateConfig , delegate ) , nil
}
// 0. in the case of a request that is not attempting to do nested impersonation
// 1. if we make the assumption that the TCR API does not issue tokens (or pass the TCR API bearer token
// authenticator into this func - we need to know the authentication cred is something KAS would honor)
// 2. then if preserve the incoming authorization header into the request's context
// 3. we could reauthenticate it here (it would be a free cache hit)
// 4. confirm that it matches the passed in user info (i.e. it was actually the cred used to authenticate and not a client cert)
// 5. then we could issue a reverse proxy request using an anonymous rest config and the bearer token
// 6. thus instead of impersonating the user, we would just be passing their request through
// 7. this would preserve the UID info and thus allow us to safely support all token based auth
// 8. the above would be safe even if in the future Kube started supporting UIDs asserted by client certs
return nil , constable . Error ( "unexpected uid" )
}
2021-03-14 01:25:23 +00:00
func newInternalErrResponse ( w http . ResponseWriter , r * http . Request , s runtime . NegotiatedSerializer , msg string ) {
newStatusErrResponse ( w , r , s , apierrors . NewInternalError ( constable . Error ( msg ) ) )
}
func newStatusErrResponse ( w http . ResponseWriter , r * http . Request , s runtime . NegotiatedSerializer , err * apierrors . StatusError ) {
requestInfo , ok := genericapirequest . RequestInfoFrom ( r . Context ( ) )
if ! ok {
responsewriters . InternalError ( w , r , constable . Error ( "no RequestInfo found in the context" ) )
return
}
gv := schema . GroupVersion { Group : requestInfo . APIGroup , Version : requestInfo . APIVersion }
responsewriters . ErrorNegotiated ( err , s , gv , w , r )
}
2021-03-18 19:32:33 +00:00
func getTransportForProtocol ( restConfig * rest . Config , protocol string ) ( http . RoundTripper , error ) {
transportConfig , err := restConfig . TransportConfig ( )
if err != nil {
return nil , fmt . Errorf ( "could not get in-cluster transport config: %w" , err )
}
transportConfig . TLS . NextProtos = [ ] string { protocol }
return transport . New ( transportConfig )
}