sts: add support for certificate-based authentication (#12748)

This commit adds a new STS API for X.509 certificate
authentication.

A client can make an HTTP POST request over a TLS connection
and MinIO will verify the provided client certificate, map it to an 
S3 policy and return temp. S3 credentials to the client.

So, this STS API allows clients to authenticate with X.509
certificates over TLS and obtain temp. S3 credentials.

For more details and examples refer to the docs/sts/tls.md
documentation.

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
This commit is contained in:
Andreas Auernhammer
2021-09-08 04:03:48 +02:00
committed by GitHub
parent 43d2655ee4
commit e438dccf19
11 changed files with 396 additions and 8 deletions

View File

@@ -417,6 +417,8 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
off = !openid.Enabled(kv)
case config.IdentityLDAPSubSys:
off = !xldap.Enabled(kv)
case config.IdentityTLSSubSys:
off = !globalSTSTLSConfig.Enabled
}
if off {
s.WriteString(config.KvComment)

View File

@@ -34,6 +34,7 @@ import (
"github.com/minio/minio/internal/config/heal"
xldap "github.com/minio/minio/internal/config/identity/ldap"
"github.com/minio/minio/internal/config/identity/openid"
xtls "github.com/minio/minio/internal/config/identity/tls"
"github.com/minio/minio/internal/config/notify"
"github.com/minio/minio/internal/config/policy/opa"
"github.com/minio/minio/internal/config/scanner"
@@ -54,6 +55,7 @@ func initHelp() {
config.CompressionSubSys: compress.DefaultKVS,
config.IdentityLDAPSubSys: xldap.DefaultKVS,
config.IdentityOpenIDSubSys: openid.DefaultKVS,
config.IdentityTLSSubSys: xtls.DefaultKVS,
config.PolicyOPASubSys: opa.DefaultKVS,
config.RegionSubSys: config.DefaultRegionKVS,
config.APISubSys: api.DefaultKVS,
@@ -98,6 +100,10 @@ func initHelp() {
Key: config.IdentityLDAPSubSys,
Description: "enable LDAP SSO support",
},
config.HelpKV{
Key: config.IdentityTLSSubSys,
Description: "enable X.509 TLS certificate SSO support",
},
config.HelpKV{
Key: config.PolicyOPASubSys,
Description: "[DEPRECATED] enable external OPA for policy enforcement",
@@ -202,6 +208,7 @@ func initHelp() {
config.ScannerSubSys: scanner.Help,
config.IdentityOpenIDSubSys: openid.Help,
config.IdentityLDAPSubSys: xldap.Help,
config.IdentityTLSSubSys: xtls.Help,
config.PolicyOPASubSys: opa.Help,
config.LoggerWebhookSubSys: logger.Help,
config.AuditWebhookSubSys: logger.HelpWebhook,
@@ -317,6 +324,12 @@ func validateConfig(s config.Config) error {
conn.Close()
}
}
{
_, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default])
if err != nil {
return err
}
}
if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
@@ -469,6 +482,11 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
logger.Fatal(errors.New("no KMS configured"), "MINIO_KMS_AUTO_ENCRYPTION requires a valid KMS configuration")
}
globalSTSTLSConfig, err = xtls.Lookup(s[config.IdentityTLSSubSys][config.Default])
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize X.509/TLS STS API: %w", err))
}
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
NewGatewayHTTPTransport(), xhttp.DrainBody)
if err != nil {

View File

@@ -38,6 +38,7 @@ import (
"github.com/minio/minio/internal/config/dns"
xldap "github.com/minio/minio/internal/config/identity/ldap"
"github.com/minio/minio/internal/config/identity/openid"
xtls "github.com/minio/minio/internal/config/identity/tls"
"github.com/minio/minio/internal/config/policy/opa"
"github.com/minio/minio/internal/config/storageclass"
xhttp "github.com/minio/minio/internal/http"
@@ -188,6 +189,7 @@ var (
globalStorageClass storageclass.Config
globalLDAPConfig xldap.Config
globalOpenIDConfig openid.Config
globalSTSTLSConfig xtls.Config
// CA root certificates, a nil value means system certs pool will be used
globalRootCAs *x509.CertPool

View File

@@ -191,3 +191,15 @@ type AssumeRoleWithLDAPResponse struct {
type LDAPIdentityResult struct {
Credentials auth.Credentials `xml:",omitempty"`
}
// AssumeRoleWithCertificateResponse contains the result of
// a successful AssumeRoleWithCertificate request.
type AssumeRoleWithCertificateResponse struct {
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithCertificateResponse" json:"-"`
Result struct {
Credentials auth.Credentials `xml:"Credentials,omitempty"`
} `xml:"AssumeRoleWithCertificateResult"`
Metadata struct {
RequestID string `xml:"RequestId,omitempty"`
} `xml:"ResponseMetadata,omitempty"`
}

View File

@@ -93,6 +93,8 @@ const (
ErrSTSClientGrantsExpiredToken
ErrSTSInvalidClientGrantsToken
ErrSTSMalformedPolicyDocument
ErrSTSInsecureConnection
ErrSTSInvalidClientCertificate
ErrSTSNotInitialized
ErrSTSInternalError
)
@@ -145,6 +147,16 @@ var stsErrCodes = stsErrorCodeMap{
Description: "The request was rejected because the policy document was malformed.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrSTSInsecureConnection: {
Code: "InsecureConnection",
Description: "The request was made over a plain HTTP connection. A TLS connection is required.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrSTSInvalidClientCertificate: {
Code: "InvalidClientCertificate",
Description: "The provided client certificate is invalid. Retry with a different certificate.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrSTSNotInitialized: {
Code: "STSNotInitialized",
Description: "STS API not initialized, please try again.",

View File

@@ -20,11 +20,13 @@ package cmd
import (
"bytes"
"context"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/minio/minio/internal/auth"
@@ -48,10 +50,11 @@ const (
stsLDAPPassword = "LDAPPassword"
// STS API action constants
clientGrants = "AssumeRoleWithClientGrants"
webIdentity = "AssumeRoleWithWebIdentity"
ldapIdentity = "AssumeRoleWithLDAPIdentity"
assumeRole = "AssumeRole"
clientGrants = "AssumeRoleWithClientGrants"
webIdentity = "AssumeRoleWithWebIdentity"
ldapIdentity = "AssumeRoleWithLDAPIdentity"
clientCertificate = "AssumeRoleWithCertificate"
assumeRole = "AssumeRole"
stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB
@@ -124,6 +127,12 @@ func registerSTSRouter(router *mux.Router) {
Queries(stsVersion, stsAPIVersion).
Queries(stsLDAPUsername, "{LDAPUsername:.*}").
Queries(stsLDAPPassword, "{LDAPPassword:.*}")
// AssumeRoleWithCertificate
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCertificate)).
Queries(stsAction, clientCertificate).
Queries(stsVersion, stsAPIVersion)
}
func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (user auth.Credentials, isErrCodeSTS bool, stsErr STSErrorCode) {
@@ -649,3 +658,100 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *
writeSuccessResponseXML(w, encodedSuccessResponse)
}
// AssumeRoleWithCertificate implements user authentication with client certificates.
// It verifies the client-provided X.509 certificate, maps the certificate to an S3 policy
// and returns temp. S3 credentials to the client.
//
// API endpoint: https://minio:9000?Action=AssumeRoleWithCertificate&Version=2011-06-15
func (sts *stsAPIHandlers) AssumeRoleWithCertificate(w http.ResponseWriter, r *http.Request) {
var ctx = newContext(r, w, "AssumeRoleWithCertificate")
if !globalSTSTLSConfig.Enabled {
writeSTSErrorResponse(ctx, w, true, ErrSTSNotInitialized, errors.New("STS API 'AssumeRoleWithCertificate' is disabled"))
return
}
// We have to establish a TLS connection and the
// client must provide exactly one client certificate.
// Otherwise, we don't have a certificate to verify or
// the policy lookup would ambigious.
if r.TLS == nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInsecureConnection, errors.New("No TLS connection attempt"))
return
}
if len(r.TLS.PeerCertificates) == 0 {
writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, errors.New("No client certificate provided"))
return
}
if len(r.TLS.PeerCertificates) > 1 {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, errors.New("More than one client certificate provided"))
return
}
var certificate = r.TLS.PeerCertificates[0]
if !globalSTSTLSConfig.InsecureSkipVerify {
_, err := certificate.Verify(x509.VerifyOptions{
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
},
Roots: globalRootCAs,
})
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidClientCertificate, err)
return
}
}
// We map the X.509 subject common name to the policy. So, a client
// with the common name "foo" will be associated with the policy "foo".
// Other mapping functions - e.g. public-key hash based mapping - are
// possible but not implemented.
//
// Group mapping is not possible with standard X.509 certificates.
if certificate.Subject.CommonName == "" {
writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, errors.New("certificate subject CN cannot be empty"))
return
}
expiry, err := globalSTSTLSConfig.GetExpiryDuration(r.Form.Get(stsDurationSeconds))
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, err)
return
}
// We set the expiry of the temp. credentials to the minimum of the
// configured expiry and the duration until the certificate itself
// expires.
// We must not issue credentials that out-live the certificate.
if validUntil := time.Until(certificate.NotAfter); validUntil < expiry {
expiry = validUntil
}
// Associate any service accounts to the certificate CN
parentUser := "tls:" + certificate.Subject.CommonName
tmpCredentials, err := auth.GetNewCredentialsWithMetadata(map[string]interface{}{
expClaim: time.Now().UTC().Add(expiry).Unix(),
parentClaim: parentUser,
subClaim: certificate.Subject.CommonName,
audClaim: certificate.Subject.Organization,
issClaim: certificate.Issuer.CommonName,
}, globalActiveCred.SecretKey)
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
return
}
tmpCredentials.ParentUser = parentUser
err = globalIAMSys.SetTempUser(tmpCredentials.AccessKey, tmpCredentials, certificate.Subject.CommonName)
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
return
}
var response = new(AssumeRoleWithCertificateResponse)
response.Result.Credentials = tmpCredentials
response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
writeSuccessResponseXML(w, encodeResponse(response))
}

View File

@@ -16,13 +16,15 @@ func _() {
_ = x[ErrSTSClientGrantsExpiredToken-5]
_ = x[ErrSTSInvalidClientGrantsToken-6]
_ = x[ErrSTSMalformedPolicyDocument-7]
_ = x[ErrSTSNotInitialized-8]
_ = x[ErrSTSInternalError-9]
_ = x[ErrSTSInsecureConnection-8]
_ = x[ErrSTSInvalidClientCertificate-9]
_ = x[ErrSTSNotInitialized-10]
_ = x[ErrSTSInternalError-11]
}
const _STSErrorCode_name = "STSNoneSTSAccessDeniedSTSMissingParameterSTSInvalidParameterValueSTSWebIdentityExpiredTokenSTSClientGrantsExpiredTokenSTSInvalidClientGrantsTokenSTSMalformedPolicyDocumentSTSNotInitializedSTSInternalError"
const _STSErrorCode_name = "STSNoneSTSAccessDeniedSTSMissingParameterSTSInvalidParameterValueSTSWebIdentityExpiredTokenSTSClientGrantsExpiredTokenSTSInvalidClientGrantsTokenSTSMalformedPolicyDocumentSTSInsecureConnectionSTSInvalidClientCertificateSTSNotInitializedSTSInternalError"
var _STSErrorCode_index = [...]uint8{0, 7, 22, 41, 65, 91, 118, 145, 171, 188, 204}
var _STSErrorCode_index = [...]uint8{0, 7, 22, 41, 65, 91, 118, 145, 171, 192, 219, 236, 252}
func (i STSErrorCode) String() string {
if i < 0 || i >= STSErrorCode(len(_STSErrorCode_index)-1) {