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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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"
@ -51,6 +53,7 @@ const (
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) {

107
docs/sts/tls.md Normal file
View File

@ -0,0 +1,107 @@
# AssumeRoleWithCertificate [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
## Introduction
MinIO provides a custom STS API that allows authentication with client X.509 / TLS certificates.
A major advantage of certificate-based authentication compared to other STS authentication methods, like OpenID Connect or LDAP/AD, is that client authentication works without any additional/external component that must be constantly available. Therefore, certificate-based authentication may provide better availability / lower operational complexity.
The MinIO TLS STS API can be configured via MinIO's standard configuration API (i.e. using `mc admin config set/get`). Further, it can be configured via the following environment variables:
```
mc admin config set myminio identity_tls --env
KEY:
identity_tls enable X.509 TLS certificate SSO support
ARGS:
MINIO_IDENTITY_TLS_SKIP_VERIFY (on|off) trust client certificates without verification. Defaults to "off" (verify)
```
The MinIO TLS STS API is enabled by default. However, it can be completely *disabled* by setting:
```
MINIO_IDENTITY_TLS_ENABLE=off
```
## Example
MinIO exposes a custom S3 STS API endpoint as `Action=AssumeRoleWithCertificate`. A client has to send an HTTP `POST` request to `https://<host>:<port>?Action=AssumeRoleWithCertificate&Version=2011-06-15`. Since the authentication and authorization happens via X.509 certificates the client has to send the request over **TLS** and has to provide
a client certificate.
The following curl example shows how to authenticate to a MinIO server with client certificate and obtain STS access credentials.
```curl
curl -X POST --key private.key --cert public.crt "https://minio:9000?Action=AssumeRoleWithCertificate&Version=2011-06-15&DurationSeconds=3600"
```
```xml
<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleWithCertificateResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleWithCertificateResult>
<Credentials>
<AccessKeyId>YC12ZBHUVW588BQAE5BM</AccessKeyId>
<SecretAccessKey>Zgl9+zdE0pZ88+hLqtfh0ocLN+WQTJixHouCkZkW</SecretAccessKey>
<Expiration>2021-07-19T20:10:45Z</Expiration
<SessionToken>eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJZQzEyWkJIVVZXNTg4QlFBRTVCTSIsImV4cCI6MTYyNjcyNTQ0NX0.wvMUf3w_x16qpVWgua8WxnV1Sgtv1jOnSu03vbrwOMzV3cI4q3_9WZD9LwlP-34DTsvbsg7gCBGh6YNriMMiQw</SessionToken>
</Credentials>
</AssumeRoleWithCertificateResult>
<ResponseMetadata>
<RequestId>169339CD8B3A6948</RequestId>
</ResponseMetadata>
</AssumeRoleWithCertificateResponse>
```
## Authentication Flow
A client can request temp. S3 credentials via the STS API. It can authenticate via a client certificate and obtain a access/secret key pair as well as a session token. These credentials are associated to an S3 policy at the MinIO server.
In case of certificate-based authentication, MinIO has to map the client-provided certificate to an S3 policy. MinIO does this via the subject common name field of the X.509 certificate. So, MinIO will associate a certificate with a subject `CN = foobar` to a S3 policy named `foobar`.
The following self-signed certificate is issued for `consoleAdmin`. So, MinIO would associate it with the pre-defined `consoleAdmin` policy.
```
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
35:ac:60:46:ad:8d:de:18:dc:0b:f6:98:14:ee:89:e8
Signature Algorithm: ED25519
Issuer: CN = consoleAdmin
Validity
Not Before: Jul 19 15:08:44 2021 GMT
Not After : Aug 18 15:08:44 2021 GMT
Subject: CN = consoleAdmin
Subject Public Key Info:
Public Key Algorithm: ED25519
ED25519 Public-Key:
pub:
5a:91:87:b8:77:fe:d4:af:d9:c7:c7:ce:55:ae:74:
aa:f3:f1:fe:04:63:9b:cb:20:97:61:97:90:94:fa:
12:8b
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
Signature Algorithm: ED25519
7e:aa:be:ed:47:4d:b9:2f:fc:ed:7f:5a:fc:6b:c0:05:5b:f5:
a0:31:fe:86:e3:8e:3f:49:af:6d:d5:ac:c7:c4:57:47:ce:97:
7d:ab:b8:e9:75:ec:b4:39:fb:c8:cf:53:16:5b:1f:15:b6:7f:
5a:d1:35:2d:fc:31:3a:10:e7:0c
```
> Observe the `Subject: CN = consoleAdmin` field.
Also, note that the certificate has to contain the `Extended Key Usage: TLS Web Client Authentication`. Otherwise, MinIO would not accept the certificate as client certificate.
Now, the STS certificate-based authentication happens in 4 steps:
- Client sends HTTP `POST` request over a TLS connection hitting the MinIO TLS STS API.
- MinIO verifies that the client certificate is valid.
- MinIO tries to find a policy that matches the `CN` of the client certificate.
- MinIO returns temp. S3 credentials associated to the found policy.
The returned credentials expiry after a certain period of time that can be configured via `&DurationSeconds=3600`. By default, the STS credentials are valid for 1 hour. The minimum expiration allowed is 15 minutes.
Further, the temp. S3 credentials will never out-live the client certificate. For example, if the `MINIO_IDENTITY_TLS_STS_EXPIRY` is 7 days but the certificate itself is only valid for the next 3 days, then MinIO will return S3 credentials that are valid for 3 days only.
## Explore Further
- [MinIO Admin Complete Guide](https://docs.min.io/docs/minio-admin-complete-guide.html)
- [The MinIO documentation website](https://docs.min.io)

View File

@ -66,6 +66,7 @@ const (
PolicyOPASubSys = "policy_opa"
IdentityOpenIDSubSys = "identity_openid"
IdentityLDAPSubSys = "identity_ldap"
IdentityTLSSubSys = "identity_tls"
CacheSubSys = "cache"
RegionSubSys = "region"
EtcdSubSys = "etcd"
@ -113,6 +114,7 @@ var SubSystems = set.CreateStringSet(
PolicyOPASubSys,
IdentityLDAPSubSys,
IdentityOpenIDSubSys,
IdentityTLSSubSys,
ScannerSubSys,
HealSubSys,
NotifyAMQPSubSys,
@ -147,6 +149,7 @@ var SubSystemsSingleTargets = set.CreateStringSet([]string{
PolicyOPASubSys,
IdentityLDAPSubSys,
IdentityOpenIDSubSys,
IdentityTLSSubSys,
HealSubSys,
ScannerSubSys,
}...)

View File

@ -0,0 +1,123 @@
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package tls
import (
"strconv"
"time"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/env"
)
const (
// EnvEnabled is an environment variable that controls whether the X.509
// TLS STS API is enabled. By default, if not set, it is enabled.
EnvEnabled = "MINIO_IDENTITY_TLS_ENABLE"
// EnvSkipVerify is an environment variable that controls whether
// MinIO verifies the client certificate present by the client
// when requesting temp. credentials.
// By default, MinIO always verify the client certificate.
//
// The client certificate verification should only be skipped
// when debugging or testing a setup since it allows arbitrary
// clients to obtain temp. credentials with arbitrary policy
// permissions - including admin permissions.
EnvSkipVerify = "MINIO_IDENTITY_TLS_SKIP_VERIFY"
)
// Config contains the STS TLS configuration for generating temp.
// credentials and mapping client certificates to S3 policies.
type Config struct {
Enabled bool `json:"enabled"`
// InsecureSkipVerify, if set to true, disables the client
// certificate verification. It should only be set for
// debugging or testing purposes.
InsecureSkipVerify bool `json:"skip_verify"`
}
const (
defaultExpiry time.Duration = 1 * time.Hour
minExpiry time.Duration = 15 * time.Minute
maxExpiry time.Duration = 365 * 24 * time.Hour
)
// GetExpiryDuration - return parsed expiry duration.
func (l Config) GetExpiryDuration(dsecs string) (time.Duration, error) {
if dsecs == "" {
return defaultExpiry, nil
}
d, err := strconv.Atoi(dsecs)
if err != nil {
return 0, auth.ErrInvalidDuration
}
dur := time.Duration(d) * time.Second
if dur < minExpiry || dur > maxExpiry {
return 0, auth.ErrInvalidDuration
}
return dur, nil
}
// Lookup returns a new Config by merging the given K/V config
// system with environment variables.
func Lookup(kvs config.KVS) (Config, error) {
if err := config.CheckValidKeys(config.IdentityTLSSubSys, kvs, DefaultKVS); err != nil {
return Config{}, err
}
insecureSkipVerify, err := config.ParseBool(env.Get(EnvSkipVerify, kvs.Get(skipVerify)))
if err != nil {
return Config{}, err
}
enabled, err := config.ParseBool(env.Get(EnvEnabled, "on"))
if err != nil {
return Config{}, err
}
return Config{
Enabled: enabled,
InsecureSkipVerify: insecureSkipVerify,
}, nil
}
const (
skipVerify = "skip_verify"
)
// DefaultKVS is the the default K/V config system for
// the STS TLS API.
var DefaultKVS = config.KVS{
config.KV{
Key: skipVerify,
Value: "off",
},
}
// Help is the help and description for the STS API K/V configuration.
var Help = config.HelpKVS{
config.HelpKV{
Key: skipVerify,
Description: `trust client certificates without verification. Defaults to "off" (verify)`,
Optional: true,
Type: "on|off",
},
}

View File

@ -172,6 +172,7 @@ func NewServer(addrs []string, handler http.Handler, getCert certs.GetCertificat
MinVersion: tls.VersionTLS12,
NextProtos: []string{"http/1.1", "h2"},
GetCertificate: getCert,
ClientAuth: tls.RequestClientCert,
}
if secureCiphers || fips.Enabled {
tlsConfig.CipherSuites = fips.CipherSuitesTLS()