Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bootstrap kube:admin OAuth user #21580

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions hack/import-restrictions.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@
"ignoredSubTrees": [
"github.com/openshift/origin/pkg/oauthserver",

"github.com/openshift/origin/pkg/apiserver/authentication/oauth",
"github.com/openshift/origin/pkg/oauth/apis/oauth/validation",

"github.com/openshift/origin/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver",
"github.com/openshift/origin/pkg/cmd/server/origin",
"github.com/openshift/origin/pkg/cmd/server/apis/config/validation",
Expand Down
62 changes: 62 additions & 0 deletions pkg/apiserver/authentication/oauth/bootstrapauthenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package oauth

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
kauthenticator "k8s.io/apiserver/pkg/authentication/authenticator"
kuser "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/typed/core/v1"

userapi "github.com/openshift/api/user/v1"
oauthclient "github.com/openshift/client-go/oauth/clientset/versioned/typed/oauth/v1"
"github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap"
)

type bootstrapAuthenticator struct {
tokens oauthclient.OAuthAccessTokenInterface
secrets v1.SecretInterface
validator OAuthTokenValidator
}

func NewBootstrapAuthenticator(tokens oauthclient.OAuthAccessTokenInterface, secrets v1.SecretsGetter, validators ...OAuthTokenValidator) kauthenticator.Token {
return &bootstrapAuthenticator{
tokens: tokens,
secrets: secrets.Secrets(metav1.NamespaceSystem),
validator: OAuthTokenValidators(validators),
}
}

func (a *bootstrapAuthenticator) AuthenticateToken(name string) (kuser.Info, bool, error) {
token, err := a.tokens.Get(name, metav1.GetOptions{})
if err != nil {
return nil, false, errLookup // mask the error so we do not leak token data in logs
}

if token.UserName != bootstrap.BootstrapUser {
return nil, false, nil
}

_, uid, ok, err := bootstrap.HashAndUID(a.secrets)
if err != nil || !ok {
return nil, ok, err
}

// this allows us to reuse existing validators
// since the uid is based on the secret, if the secret changes, all
// tokens issued for the bootstrap user before that change stop working
fakeUser := &userapi.User{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID(uid),
},
}

if err := a.validator.Validate(token, fakeUser); err != nil {
return nil, false, err
}

// we explicitly do not set UID as we do not want to leak any derivative of the password
return &kuser.DefaultInfo{
Name: bootstrap.BootstrapUser,
Groups: []string{kuser.SystemPrivilegedGroup}, // authorized to do everything
}, true, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
genericapiserver "k8s.io/apiserver/pkg/server"
webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
kclientsetexternal "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/util/cert"
sacontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
Expand Down Expand Up @@ -79,10 +80,22 @@ func NewAuthenticator(
userClient.User().Users(),
apiClientCAs,
usercache.NewGroupCache(groupInformer),
kubeExternalClient.CoreV1(),
)
}

func newAuthenticator(serviceAccountPublicKeyFiles []string, oauthConfig *osinv1.OAuthConfig, authConfig kubecontrolplanev1.MasterAuthConfig, accessTokenGetter oauthclient.OAuthAccessTokenInterface, oauthClientLister oauthclientlister.OAuthClientLister, tokenGetter serviceaccount.ServiceAccountTokenGetter, userGetter usertypedclient.UserInterface, apiClientCAs *x509.CertPool, groupMapper oauth.UserToGroupMapper) (authenticator.Request, map[string]genericapiserver.PostStartHookFunc, error) {
func newAuthenticator(
serviceAccountPublicKeyFiles []string,
oauthConfig *osinv1.OAuthConfig,
authConfig kubecontrolplanev1.MasterAuthConfig,
accessTokenGetter oauthclient.OAuthAccessTokenInterface,
oauthClientLister oauthclientlister.OAuthClientLister,
tokenGetter serviceaccount.ServiceAccountTokenGetter,
userGetter usertypedclient.UserInterface,
apiClientCAs *x509.CertPool,
groupMapper oauth.UserToGroupMapper,
secretsGetter v1.SecretsGetter,
) (authenticator.Request, map[string]genericapiserver.PostStartHookFunc, error) {
postStartHooks := map[string]genericapiserver.PostStartHookFunc{}
authenticators := []authenticator.Request{}
tokenAuthenticators := []authenticator.Token{}
Expand Down Expand Up @@ -121,6 +134,12 @@ func newAuthenticator(serviceAccountPublicKeyFiles []string, oauthConfig *osinv1
tokenAuthenticators = append(tokenAuthenticators,
// if you have an OAuth bearer token, you're a human (usually)
group.NewTokenGroupAdder(oauthTokenAuthenticator, []string{bootstrappolicy.AuthenticatedOAuthGroup}))

if oauthConfig.SessionConfig != nil {
tokenAuthenticators = append(tokenAuthenticators,
// bootstrap oauth user that can do anything, backed by a secret
oauth.NewBootstrapAuthenticator(accessTokenGetter, secretsGetter, validators...))
}
}

for _, wta := range authConfig.WebhookTokenAuthenticators {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import (
"net/http"

osinv1 "github.com/openshift/api/osin/v1"
routeclient "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
"github.com/openshift/origin/pkg/oauthserver/oauthserver"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/kubernetes"
)

// TODO this is taking a very large config for a small piece of it. The information must be broken up at some point so that
Expand All @@ -23,18 +21,6 @@ func NewOAuthServerConfigFromMasterConfig(genericConfig *genericapiserver.Config
oauthServerConfig.GenericConfig.AuditBackend = genericConfig.AuditBackend
oauthServerConfig.GenericConfig.AuditPolicyChecker = genericConfig.AuditPolicyChecker

routeClient, err := routeclient.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}
kubeClient, err := kubernetes.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}

oauthServerConfig.ExtraOAuthConfig.RouteClient = routeClient
oauthServerConfig.ExtraOAuthConfig.KubeClient = kubeClient

// Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to
oauthServerConfig.ExtraOAuthConfig.AssetPublicAddresses = []string{oauthConfig.AssetPublicURL}

Expand Down
14 changes: 0 additions & 14 deletions pkg/cmd/openshift-osinserver/server.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package openshift_osinserver

import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"

routeclient "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
configapi "github.com/openshift/origin/pkg/cmd/server/apis/config"
"github.com/openshift/origin/pkg/oauthserver/oauthserver"
genericapiserver "k8s.io/apiserver/pkg/server"
Expand All @@ -25,18 +23,6 @@ func RunOpenShiftOsinServer(oauthConfig configapi.OAuthConfig, kubeClientConfig
//oauthServerConfig.GenericConfig.AuditBackend = genericConfig.AuditBackend
//oauthServerConfig.GenericConfig.AuditPolicyChecker = genericConfig.AuditPolicyChecker

routeClient, err := routeclient.NewForConfig(kubeClientConfig)
if err != nil {
return err
}
kubeClient, err := kubernetes.NewForConfig(kubeClientConfig)
if err != nil {
return err
}

oauthServerConfig.ExtraOAuthConfig.RouteClient = routeClient
oauthServerConfig.ExtraOAuthConfig.KubeClient = kubeClient

// Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to
oauthServerConfig.ExtraOAuthConfig.AssetPublicAddresses = []string{oauthConfig.AssetPublicURL}

Expand Down
11 changes: 11 additions & 0 deletions pkg/cmd/server/apis/config/bootstrapidp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package config

import "k8s.io/apimachinery/pkg/apis/meta/v1"

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// BootstrapIdentityProvider serves as a marker for an "IDP" that is backed by osin
// this allows us to reuse most of the logic from existing identity providers
type BootstrapIdentityProvider struct {
v1.TypeMeta
}
6 changes: 5 additions & 1 deletion pkg/cmd/server/apis/config/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,11 @@ func IsPasswordAuthenticator(provider IdentityProvider) bool {
*DenyAllPasswordIdentityProvider,
*HTPasswdPasswordIdentityProvider,
*LDAPPasswordIdentityProvider,
*KeystonePasswordIdentityProvider:
*KeystonePasswordIdentityProvider,
// we explicitly only include the bootstrap type in this function
// but not IsIdentityProviderType as this is not a real IDP
// it is an implementation detail that is not surfaced to users
*BootstrapIdentityProvider:

return true
}
Expand Down
25 changes: 25 additions & 0 deletions pkg/cmd/server/apis/config/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 0 additions & 14 deletions pkg/cmd/server/origin/legacyconfigprocessing/patch_oauthserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import (

"github.com/openshift/origin/pkg/oauthserver/oauthserver"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/kubernetes"

routeclient "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
configapi "github.com/openshift/origin/pkg/cmd/server/apis/config"
)

Expand All @@ -24,18 +22,6 @@ func NewOAuthServerConfigFromMasterConfig(genericConfig *genericapiserver.Config
oauthServerConfig.GenericConfig.AuditBackend = genericConfig.AuditBackend
oauthServerConfig.GenericConfig.AuditPolicyChecker = genericConfig.AuditPolicyChecker

routeClient, err := routeclient.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}
kubeClient, err := kubernetes.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}

oauthServerConfig.ExtraOAuthConfig.RouteClient = routeClient
oauthServerConfig.ExtraOAuthConfig.KubeClient = kubeClient

// Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to
oauthServerConfig.ExtraOAuthConfig.AssetPublicAddresses = []string{oauthConfig.AssetPublicURL}

Expand Down
9 changes: 8 additions & 1 deletion pkg/oauth/apis/oauth/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

authorizerscopes "github.com/openshift/origin/pkg/authorization/authorizer/scope"
oauthapi "github.com/openshift/origin/pkg/oauth/apis/oauth"
"github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap"
uservalidation "github.com/openshift/origin/pkg/user/apis/user/validation"
)

Expand Down Expand Up @@ -312,7 +313,13 @@ func ValidateClientNameField(value string, fldPath *field.Path) field.ErrorList
func ValidateUserNameField(value string, fldPath *field.Path) field.ErrorList {
if len(value) == 0 {
return field.ErrorList{field.Required(fldPath, "")}
} else if reasons := uservalidation.ValidateUserName(value, false); len(reasons) != 0 {
}
// we explicitly allow the bootstrap user in the username field
// note that we still do not allow the user API objects to have such a name
if value == bootstrap.BootstrapUser {
return field.ErrorList{}
}
if reasons := uservalidation.ValidateUserName(value, false); len(reasons) != 0 {
return field.ErrorList{field.Invalid(fldPath, value, strings.Join(reasons, ", "))}
}
return field.ErrorList{}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (strategy) DefaultGarbageCollectionPolicy(ctx context.Context) rest.Garbage

func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
auth := obj.(*oauthapi.OAuthClientAuthorization)
// this is not as easy to break apart in the face of the bootstrap user
auth.Name = fmt.Sprintf("%s:%s", auth.UserName, auth.ClientName)
}

Expand All @@ -50,6 +51,7 @@ func (strategy) GenerateName(base string) string {

func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
auth := obj.(*oauthapi.OAuthClientAuthorization)
// this is not as easy to break apart in the face of the bootstrap user
auth.Name = fmt.Sprintf("%s:%s", auth.UserName, auth.ClientName)
}

Expand Down
84 changes: 84 additions & 0 deletions pkg/oauthserver/authenticator/password/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package bootstrap

import (
"crypto/sha512"
"encoding/base64"

"golang.org/x/crypto/bcrypt"

"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/typed/core/v1"
)

const (
// BootstrapUser is the magic bootstrap OAuth user that can perform any action
BootstrapUser = "kube:admin"
// support basic auth which does not allow : in username
bootstrapUserBasicAuth = "kubeadmin"
)

func New(secrets v1.SecretsGetter) authenticator.Password {
return &bootstrapPassword{
secrets: secrets.Secrets(metav1.NamespaceSystem),
names: sets.NewString(BootstrapUser, bootstrapUserBasicAuth),
}
}

type bootstrapPassword struct {
secrets v1.SecretInterface
names sets.String
}

func (b *bootstrapPassword) AuthenticatePassword(username, password string) (user.Info, bool, error) {
if !b.names.Has(username) {
return nil, false, nil
}

hashedPassword, uid, ok, err := HashAndUID(b.secrets)
if err != nil || !ok {
return nil, ok, err
}

if err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)); err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
return nil, false, nil
}
return nil, false, err
}

// do not set other fields, see identitymapper.userToInfo func
return &user.DefaultInfo{
Name: BootstrapUser,
UID: uid, // uid ties this authentication to the current state of the secret
}, true, nil
}

func HashAndUID(secrets v1.SecretInterface) ([]byte, string, bool, error) {
secret, err := secrets.Get(bootstrapUserBasicAuth, metav1.GetOptions{})
if errors.IsNotFound(err) {
return nil, "", false, nil
}
if err != nil {
return nil, "", false, err
}

hashedPassword := secret.Data[bootstrapUserBasicAuth]

// make sure the value is a valid bcrypt hash
if _, err := bcrypt.Cost(hashedPassword); err != nil {
return nil, "", false, err
}

exactSecret := string(secret.UID) + secret.ResourceVersion
both := append([]byte(exactSecret), hashedPassword...)

// use a hash to avoid leaking any derivative of the password
// this makes it easy for us to tell if the secret changed
uidBytes := sha512.Sum512(both)

return hashedPassword, base64.RawURLEncoding.EncodeToString(uidBytes[:]), true, nil
}
2 changes: 1 addition & 1 deletion pkg/oauthserver/oauth/handlers/default_auth_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func NewUnionAuthenticationHandler(passedChallengers map[string]AuthenticationCh
redirectors = new(AuthenticationRedirectors)
}

return &unionAuthenticationHandler{challengers, redirectors, errorHandler, selectionHandler}
return &unionAuthenticationHandler{challengers: challengers, redirectors: redirectors, errorHandler: errorHandler, selectionHandler: selectionHandler}
}

const (
Expand Down
Loading