decouple service accounts from root credentials (#14534)

changing root credentials makes service accounts
in-operable, this PR changes the way sessionToken
is generated for service accounts.

It changes service account behavior to generate
sessionToken claims from its own secret instead
of using global root credential.

Existing credentials will be supported by
falling back to verify using root credential.

fixes #14530
This commit is contained in:
Harshavardhana 2022-03-14 09:09:22 -07:00 committed by GitHub
parent cf94d1f1f1
commit 9c846106fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 123 additions and 47 deletions

View File

@ -197,13 +197,7 @@ func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
return claims return claims
} }
// Fetch claims in the security token returned by the client. func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{}, error) {
func getClaimsFromToken(token string) (map[string]interface{}, error) {
if token == "" {
claims := xjwt.NewMapClaims()
return claims.Map(), nil
}
// JWT token for x-amz-security-token is signed with admin // JWT token for x-amz-security-token is signed with admin
// secret key, temporary credentials become invalid if // secret key, temporary credentials become invalid if
// server admin credentials change. This is done to ensure // server admin credentials change. This is done to ensure
@ -212,10 +206,16 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
// hijacking the policies. We need to make sure that this is // hijacking the policies. We need to make sure that this is
// based an admin credential such that token cannot be decoded // based an admin credential such that token cannot be decoded
// on the client side and is treated like an opaque value. // on the client side and is treated like an opaque value.
claims, err := auth.ExtractClaims(token, globalActiveCred.SecretKey) claims, err := auth.ExtractClaims(token, secret)
if err != nil {
if subtle.ConstantTimeCompare([]byte(secret), []byte(globalActiveCred.SecretKey)) == 1 {
return nil, errAuthentication
}
claims, err = auth.ExtractClaims(token, globalActiveCred.SecretKey)
if err != nil { if err != nil {
return nil, errAuthentication return nil, errAuthentication
} }
}
// If OPA is set, return without any further checks. // If OPA is set, return without any further checks.
if globalPolicyOPA != nil { if globalPolicyOPA != nil {
@ -241,23 +241,50 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
return claims.Map(), nil return claims.Map(), nil
} }
// Fetch claims in the security token returned by the client.
func getClaimsFromToken(token string) (map[string]interface{}, error) {
return getClaimsFromTokenWithSecret(token, globalActiveCred.SecretKey)
}
// Fetch claims in the security token returned by the client and validate the token. // Fetch claims in the security token returned by the client and validate the token.
func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) { func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) {
token := getSessionToken(r) token := getSessionToken(r)
if token != "" && cred.AccessKey == "" { if token != "" && cred.AccessKey == "" {
// x-amz-security-token is not allowed for anonymous access.
return nil, ErrNoAccessKey return nil, ErrNoAccessKey
} }
if cred.IsServiceAccount() && token == "" {
token = cred.SessionToken if token == "" && cred.IsTemp() {
} // Temporary credentials should always have x-amz-security-token
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
return nil, ErrInvalidToken return nil, ErrInvalidToken
} }
claims, err := getClaimsFromToken(token)
if token != "" && !cred.IsTemp() {
// x-amz-security-token should not present for static credentials.
return nil, ErrInvalidToken
}
if cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
// validate token for temporary credentials only.
return nil, ErrInvalidToken
}
secret := globalActiveCred.SecretKey
if cred.IsServiceAccount() {
token = cred.SessionToken
secret = cred.SecretKey
}
if token != "" {
claims, err := getClaimsFromTokenWithSecret(token, secret)
if err != nil { if err != nil {
return nil, toAPIErrorCode(r.Context(), err) return nil, toAPIErrorCode(r.Context(), err)
} }
return claims, ErrNone return claims, ErrNone
}
claims := xjwt.NewMapClaims()
return claims.Map(), ErrNone
} }
// Check request auth type verifies the incoming http request // Check request auth type verifies the incoming http request

View File

@ -1456,13 +1456,28 @@ func (store *IAMStoreSys) GetAllParentUsers() map[string]string {
res := map[string]string{} res := map[string]string{}
for _, cred := range cache.iamUsersMap { for _, cred := range cache.iamUsersMap {
if cred.IsServiceAccount() || cred.IsTemp() { if (cred.IsServiceAccount() || cred.IsTemp()) && cred.SessionToken != "" {
parentUser := cred.ParentUser parentUser := cred.ParentUser
if cred.SessionToken != "" {
claims, err := getClaimsFromToken(cred.SessionToken) var (
err error
claims map[string]interface{}
)
if cred.IsServiceAccount() {
claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, cred.SecretKey)
if err != nil {
claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, globalActiveCred.SecretKey)
}
} else if cred.IsTemp() {
claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, globalActiveCred.SecretKey)
}
if err != nil { if err != nil {
continue continue
} }
if len(claims) > 0 {
if v, ok := claims[subClaim]; ok { if v, ok := claims[subClaim]; ok {
subFromToken, ok := v.(string) subFromToken, ok := v.(string)
if ok { if ok {
@ -1470,6 +1485,11 @@ func (store *IAMStoreSys) GetAllParentUsers() map[string]string {
} }
} }
} }
if parentUser == "" {
continue
}
if _, ok := res[parentUser]; !ok { if _, ok := res[parentUser]; !ok {
res[parentUser] = cred.ParentUser res[parentUser] = cred.ParentUser
} }
@ -1561,6 +1581,7 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
return errNoSuchServiceAccount return errNoSuchServiceAccount
} }
currentSecretKey := cr.SecretKey
if opts.secretKey != "" { if opts.secretKey != "" {
if !auth.IsSecretKeyValid(opts.secretKey) { if !auth.IsSecretKeyValid(opts.secretKey) {
return auth.ErrInvalidSecretKeyLength return auth.ErrInvalidSecretKeyLength
@ -1582,20 +1603,21 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
return errors.New("unknown account status value") return errors.New("unknown account status value")
} }
if opts.sessionPolicy != nil { m, err := getClaimsFromTokenWithSecret(cr.SessionToken, currentSecretKey)
m, err := getClaimsFromToken(cr.SessionToken)
if err != nil { if err != nil {
return fmt.Errorf("unable to get svc acc claims: %v", err) return fmt.Errorf("unable to get svc acc claims: %v", err)
} }
err = opts.sessionPolicy.Validate() if opts.sessionPolicy != nil {
if err != nil { if err := opts.sessionPolicy.Validate(); err != nil {
return err return err
} }
policyBuf, err := json.Marshal(opts.sessionPolicy) policyBuf, err := json.Marshal(opts.sessionPolicy)
if err != nil { if err != nil {
return err return err
} }
if len(policyBuf) > 16*humanize.KiByte { if len(policyBuf) > 16*humanize.KiByte {
return fmt.Errorf("Session policy should not exceed 16 KiB characters") return fmt.Errorf("Session policy should not exceed 16 KiB characters")
} }
@ -1603,11 +1625,12 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
// Overwrite session policy claims. // Overwrite session policy claims.
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf) m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
m[iamPolicyClaimNameSA()] = "embedded-policy" m[iamPolicyClaimNameSA()] = "embedded-policy"
cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, globalActiveCred.SecretKey) }
cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, cr.SecretKey)
if err != nil { if err != nil {
return err return err
} }
}
u := newUserIdentity(cr) u := newUserIdentity(cr)
if err := store.saveUserIdentity(ctx, u.Credentials.AccessKey, svcUser, u); err != nil { if err := store.saveUserIdentity(ctx, u.Credentials.AccessKey, svcUser, u); err != nil {

View File

@ -37,6 +37,7 @@ import (
"github.com/minio/minio/internal/arn" "github.com/minio/minio/internal/arn"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/color" "github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/jwt"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy" iampolicy "github.com/minio/pkg/iam/policy"
etcd "go.etcd.io/etcd/client/v3" etcd "go.etcd.io/etcd/client/v3"
@ -799,14 +800,17 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
} }
} }
var cred auth.Credentials var accessKey, secretKey string
var err error var err error
if len(opts.accessKey) > 0 { if len(opts.accessKey) > 0 {
cred, err = auth.CreateNewCredentialsWithMetadata(opts.accessKey, opts.secretKey, m, globalActiveCred.SecretKey) accessKey, secretKey = opts.accessKey, opts.secretKey
} else { } else {
cred, err = auth.GetNewCredentialsWithMetadata(m, globalActiveCred.SecretKey) accessKey, secretKey, err = auth.GenerateCredentials()
if err != nil {
return auth.Credentials{}, err
} }
}
cred, err := auth.CreateNewCredentialsWithMetadata(accessKey, secretKey, m, secretKey)
if err != nil { if err != nil {
return auth.Credentials{}, err return auth.Credentials{}, err
} }
@ -880,10 +884,13 @@ func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (aut
var embeddedPolicy *iampolicy.Policy var embeddedPolicy *iampolicy.Policy
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey) jwtClaims, err := auth.ExtractClaims(sa.SessionToken, sa.SecretKey)
if err != nil {
jwtClaims, err = auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
if err != nil { if err != nil {
return auth.Credentials{}, nil, err return auth.Credentials{}, nil, err
} }
}
pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA()) pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA())
sp, spok := jwtClaims.Lookup(iampolicy.SessionPolicyName) sp, spok := jwtClaims.Lookup(iampolicy.SessionPolicyName)
if ptok && spok && pt == "embedded-policy" { if ptok && spok && pt == "embedded-policy" {
@ -915,10 +922,13 @@ func (sys *IAMSys) GetClaimsForSvcAcc(ctx context.Context, accessKey string) (ma
return nil, errNoSuchServiceAccount return nil, errNoSuchServiceAccount
} }
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey) jwtClaims, err := auth.ExtractClaims(sa.SessionToken, sa.SecretKey)
if err != nil {
jwtClaims, err = auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
return jwtClaims.Map(), nil return jwtClaims.Map(), nil
} }
@ -1063,12 +1073,28 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok { if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok {
// Try to find the ldapUsername for this // Try to find the ldapUsername for this
// parentUser by extracting JWT claims // parentUser by extracting JWT claims
jwtClaims, err := auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey) var (
if err != nil { jwtClaims *jwt.MapClaims
// skip this cred - session token seems err error
// invalid )
if cred.SessionToken == "" {
continue continue
} }
if cred.IsServiceAccount() {
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, cred.SecretKey)
if err != nil {
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
}
} else {
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
}
if err != nil {
// skip this cred - session token seems invalid
continue
}
ldapUsername, ok := jwtClaims.Lookup(ldapUserN) ldapUsername, ok := jwtClaims.Lookup(ldapUserN)
if !ok { if !ok {
// skip this cred - we dont have the // skip this cred - we dont have the

View File

@ -132,7 +132,7 @@ func authenticateURL(accessKey, secretKey string) (string, error) {
// Check if the request is authenticated. // Check if the request is authenticated.
// Returns nil if the request is authenticated. errNoAuthToken if token missing. // Returns nil if the request is authenticated. errNoAuthToken if token missing.
// Returns errAuthentication for all other errors. // Returns errAuthentication for all other errors.
func webRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, bool, error) { func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, bool, error) {
token, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req) token, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req)
if err != nil { if err != nil {
if err == jwtreq.ErrNoTokenInRequest { if err == jwtreq.ErrNoTokenInRequest {

View File

@ -149,7 +149,7 @@ func TestWebRequestAuthenticate(t *testing.T) {
} }
for i, testCase := range testCases { for i, testCase := range testCases {
_, _, _, gotErr := webRequestAuthenticate(testCase.req) _, _, _, gotErr := metricsRequestAuthenticate(testCase.req)
if testCase.expectedErr != gotErr { if testCase.expectedErr != gotErr {
t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr) t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
} }

View File

@ -674,7 +674,7 @@ func metricsHandler() http.Handler {
// AuthMiddleware checks if the bearer token is valid and authorized. // AuthMiddleware checks if the bearer token is valid and authorized.
func AuthMiddleware(h http.Handler) http.Handler { func AuthMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, groups, owner, authErr := webRequestAuthenticate(r) claims, groups, owner, authErr := metricsRequestAuthenticate(r)
if authErr != nil || !claims.VerifyIssuer("prometheus", true) { if authErr != nil || !claims.VerifyIssuer("prometheus", true) {
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
return return