mirror of
https://github.com/minio/minio.git
synced 2025-11-09 21:49:46 -05:00
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:
committed by
GitHub
parent
43d2655ee4
commit
e438dccf19
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user