add allowed_organiztaional_units parameter to cert credential backend (#5252)
Specifying the `allowed_organiztaional_units` parameter to a cert auth backend role will require client certificates to contain at least one of a list of one or more "organizational units" (OU). Example use cases: Certificates are issued to entities in an organization arrangement by organizational unit (OU). The OU may be a department, team, or any other logical grouping of resources with similar roles. The entities within the OU should be granted the same policies. ``` $ vault write auth/cert/certs/ou-engineering \ certificate=@ca.pem \ policies=engineering \ allowed_organiztaional_units=engineering $ vault write auth/cert/certs/ou-engineering \ certificate=@ca.pem \ policies=engineering \ allowed_organiztaional_units=engineering,support ```
This commit is contained in:
parent
dbae477cca
commit
d39ffc9e25
|
@ -1079,6 +1079,35 @@ func TestBackend_email_singleCert(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// Test a self-signed client with OU (root CA) that is trusted
|
||||
func TestBackend_organizationalUnit_singleCert(t *testing.T) {
|
||||
connState, err := testConnState(
|
||||
"test-fixtures/root/rootcawoucert.pem",
|
||||
"test-fixtures/root/rootcawoukey.pem",
|
||||
"test-fixtures/root/rootcawoucert.pem",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("error testing connection state: %v", err)
|
||||
}
|
||||
ca, err := ioutil.ReadFile("test-fixtures/root/rootcawoucert.pem")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
Backend: testFactory(t),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "engineering"}, false),
|
||||
testAccStepLogin(t, connState),
|
||||
testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "eng*"}, false),
|
||||
testAccStepLogin(t, connState),
|
||||
testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "engineering,finance"}, false),
|
||||
testAccStepLogin(t, connState),
|
||||
testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "foo"}, false),
|
||||
testAccStepLoginInvalid(t, connState),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Test a self-signed client with URI alt names (root CA) that is trusted
|
||||
func TestBackend_uri_singleCert(t *testing.T) {
|
||||
connState, err := testConnState(
|
||||
|
@ -1432,12 +1461,13 @@ func testAccStepListCerts(
|
|||
}
|
||||
|
||||
type allowed struct {
|
||||
names string // allowed names in the certificate, looks at common, name, dns, email [depricated]
|
||||
common_names string // allowed common names in the certificate
|
||||
dns string // allowed dns names in the SAN extension of the certificate
|
||||
emails string // allowed email names in SAN extension of the certificate
|
||||
uris string // allowed uris in SAN extension of the certificate
|
||||
ext string // required extensions in the certificate
|
||||
names string // allowed names in the certificate, looks at common, name, dns, email [depricated]
|
||||
common_names string // allowed common names in the certificate
|
||||
dns string // allowed dns names in the SAN extension of the certificate
|
||||
emails string // allowed email names in SAN extension of the certificate
|
||||
uris string // allowed uris in SAN extension of the certificate
|
||||
organizational_units string // allowed OUs in the certificate
|
||||
ext string // required extensions in the certificate
|
||||
}
|
||||
|
||||
func testAccStepCert(
|
||||
|
@ -1447,16 +1477,17 @@ func testAccStepCert(
|
|||
Path: "certs/" + name,
|
||||
ErrorOk: expectError,
|
||||
Data: map[string]interface{}{
|
||||
"certificate": string(cert),
|
||||
"policies": policies,
|
||||
"display_name": name,
|
||||
"allowed_names": testData.names,
|
||||
"allowed_common_names": testData.common_names,
|
||||
"allowed_dns_sans": testData.dns,
|
||||
"allowed_email_sans": testData.emails,
|
||||
"allowed_uri_sans": testData.uris,
|
||||
"required_extensions": testData.ext,
|
||||
"lease": 1000,
|
||||
"certificate": string(cert),
|
||||
"policies": policies,
|
||||
"display_name": name,
|
||||
"allowed_names": testData.names,
|
||||
"allowed_common_names": testData.common_names,
|
||||
"allowed_dns_sans": testData.dns,
|
||||
"allowed_email_sans": testData.emails,
|
||||
"allowed_uri_sans": testData.uris,
|
||||
"allowed_organizational_units": testData.organizational_units,
|
||||
"required_extensions": testData.ext,
|
||||
"lease": 1000,
|
||||
},
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp == nil && expectError {
|
||||
|
|
|
@ -74,6 +74,12 @@ At least one must exist in the SANs. Supports globbing.`,
|
|||
At least one must exist in the SANs. Supports globbing.`,
|
||||
},
|
||||
|
||||
"allowed_organizational_units": &framework.FieldSchema{
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `A comma-separated list of Organizational Units names.
|
||||
At least one must exist in the OU field.`,
|
||||
},
|
||||
|
||||
"required_extensions": &framework.FieldSchema{
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `A comma-separated string or array of extensions
|
||||
|
@ -179,18 +185,19 @@ func (b *backend) pathCertRead(ctx context.Context, req *logical.Request, d *fra
|
|||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"certificate": cert.Certificate,
|
||||
"display_name": cert.DisplayName,
|
||||
"policies": cert.Policies,
|
||||
"ttl": cert.TTL / time.Second,
|
||||
"max_ttl": cert.MaxTTL / time.Second,
|
||||
"period": cert.Period / time.Second,
|
||||
"allowed_names": cert.AllowedNames,
|
||||
"allowed_common_names": cert.AllowedCommonNames,
|
||||
"allowed_dns_sans": cert.AllowedDNSSANs,
|
||||
"allowed_email_sans": cert.AllowedEmailSANs,
|
||||
"allowed_uri_sans": cert.AllowedURISANs,
|
||||
"required_extensions": cert.RequiredExtensions,
|
||||
"certificate": cert.Certificate,
|
||||
"display_name": cert.DisplayName,
|
||||
"policies": cert.Policies,
|
||||
"ttl": cert.TTL / time.Second,
|
||||
"max_ttl": cert.MaxTTL / time.Second,
|
||||
"period": cert.Period / time.Second,
|
||||
"allowed_names": cert.AllowedNames,
|
||||
"allowed_common_names": cert.AllowedCommonNames,
|
||||
"allowed_dns_sans": cert.AllowedDNSSANs,
|
||||
"allowed_email_sans": cert.AllowedEmailSANs,
|
||||
"allowed_uri_sans": cert.AllowedURISANs,
|
||||
"allowed_organizational_units": cert.AllowedOrganizationalUnits,
|
||||
"required_extensions": cert.RequiredExtensions,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
@ -205,6 +212,7 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr
|
|||
allowedDNSSANs := d.Get("allowed_dns_sans").([]string)
|
||||
allowedEmailSANs := d.Get("allowed_email_sans").([]string)
|
||||
allowedURISANs := d.Get("allowed_uri_sans").([]string)
|
||||
allowedOrganizationalUnits := d.Get("allowed_organizational_units").([]string)
|
||||
requiredExtensions := d.Get("required_extensions").([]string)
|
||||
|
||||
var resp logical.Response
|
||||
|
@ -278,20 +286,21 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr
|
|||
}
|
||||
|
||||
certEntry := &CertEntry{
|
||||
Name: name,
|
||||
Certificate: certificate,
|
||||
DisplayName: displayName,
|
||||
Policies: policies,
|
||||
AllowedNames: allowedNames,
|
||||
AllowedCommonNames: allowedCommonNames,
|
||||
AllowedDNSSANs: allowedDNSSANs,
|
||||
AllowedEmailSANs: allowedEmailSANs,
|
||||
AllowedURISANs: allowedURISANs,
|
||||
RequiredExtensions: requiredExtensions,
|
||||
TTL: ttl,
|
||||
MaxTTL: maxTTL,
|
||||
Period: period,
|
||||
BoundCIDRs: parsedCIDRs,
|
||||
Name: name,
|
||||
Certificate: certificate,
|
||||
DisplayName: displayName,
|
||||
Policies: policies,
|
||||
AllowedNames: allowedNames,
|
||||
AllowedCommonNames: allowedCommonNames,
|
||||
AllowedDNSSANs: allowedDNSSANs,
|
||||
AllowedEmailSANs: allowedEmailSANs,
|
||||
AllowedURISANs: allowedURISANs,
|
||||
AllowedOrganizationalUnits: allowedOrganizationalUnits,
|
||||
RequiredExtensions: requiredExtensions,
|
||||
TTL: ttl,
|
||||
MaxTTL: maxTTL,
|
||||
Period: period,
|
||||
BoundCIDRs: parsedCIDRs,
|
||||
}
|
||||
|
||||
// Store it
|
||||
|
@ -311,20 +320,21 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr
|
|||
}
|
||||
|
||||
type CertEntry struct {
|
||||
Name string
|
||||
Certificate string
|
||||
DisplayName string
|
||||
Policies []string
|
||||
TTL time.Duration
|
||||
MaxTTL time.Duration
|
||||
Period time.Duration
|
||||
AllowedNames []string
|
||||
AllowedCommonNames []string
|
||||
AllowedDNSSANs []string
|
||||
AllowedEmailSANs []string
|
||||
AllowedURISANs []string
|
||||
RequiredExtensions []string
|
||||
BoundCIDRs []*sockaddr.SockAddrMarshaler
|
||||
Name string
|
||||
Certificate string
|
||||
DisplayName string
|
||||
Policies []string
|
||||
TTL time.Duration
|
||||
MaxTTL time.Duration
|
||||
Period time.Duration
|
||||
AllowedNames []string
|
||||
AllowedCommonNames []string
|
||||
AllowedDNSSANs []string
|
||||
AllowedEmailSANs []string
|
||||
AllowedURISANs []string
|
||||
AllowedOrganizationalUnits []string
|
||||
RequiredExtensions []string
|
||||
BoundCIDRs []*sockaddr.SockAddrMarshaler
|
||||
}
|
||||
|
||||
const pathCertHelpSyn = `
|
||||
|
|
|
@ -252,6 +252,7 @@ func (b *backend) matchesConstraints(clientCert *x509.Certificate, trustedChain
|
|||
b.matchesDNSSANs(clientCert, config) &&
|
||||
b.matchesEmailSANs(clientCert, config) &&
|
||||
b.matchesURISANs(clientCert, config) &&
|
||||
b.matchesOrganizationalUnits(clientCert, config) &&
|
||||
b.matchesCertificateExtensions(clientCert, config)
|
||||
}
|
||||
|
||||
|
@ -358,6 +359,25 @@ func (b *backend) matchesURISANs(clientCert *x509.Certificate, config *ParsedCer
|
|||
return false
|
||||
}
|
||||
|
||||
// matchesOrganizationalUnits verifies that the certificate matches at least one configurd allowed OU
|
||||
func (b *backend) matchesOrganizationalUnits(clientCert *x509.Certificate, config *ParsedCert) bool {
|
||||
// Default behavior (no OUs) is to allow all OUs
|
||||
if len(config.Entry.AllowedOrganizationalUnits) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// At least one pattern must match at least one name if any patterns are specified
|
||||
for _, allowedOrganizationalUnits := range config.Entry.AllowedOrganizationalUnits {
|
||||
for _, ou := range clientCert.Subject.OrganizationalUnit {
|
||||
if glob.Glob(allowedOrganizationalUnits, ou) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchesCertificateExtensions verifies that the certificate matches configured
|
||||
// required extensions
|
||||
func (b *backend) matchesCertificateExtensions(clientCert *x509.Certificate, config *ParsedCert) bool {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
[ req ]
|
||||
default_bits = 2048
|
||||
encrypt_key = no
|
||||
prompt = no
|
||||
default_md = sha256
|
||||
distinguished_name = dn
|
||||
req_extensions = req_v3
|
||||
|
||||
[ req_v3 ]
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[ dn ]
|
||||
CN = example.com
|
||||
OU = engineering
|
||||
|
||||
[ alt_names ]
|
||||
IP.1 = 127.0.0.1
|
||||
email = valid@example.com
|
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICpjCCAY4CAQAwLDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xFDASBgNVBAsMC2Vu
|
||||
Z2luZWVyaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsI4ZJWfQ
|
||||
mI/3qfacas7O260Iii06oTP4GoQ5QpAYvcfWKKnkXagd0fBl+hfpnrK6ojYY71Jt
|
||||
cMstVdff2Wc5D3bnQ8Hikb1TMhdAAtZDUW4QbeWAXJ4mkDq1ARRcbTvK121bmDQp
|
||||
1efepohe0mDxNCruGSHpqfayC6LOkk7XZ73VAOcPPV5OOpY8el7quUdfvElxn0vH
|
||||
KBVlFRBBW2fbY5EAHDMkmBjWr0ofpwb+vhSuQlOZgsbd20mjDwSYIbywG0tAEOoj
|
||||
pLI0pOQV5msdfbqmKYE6ZmUeL/Q/pZjYh5uxFUZ4aMD/STDaeq7GdYQYcm17WL+N
|
||||
ceal9+gKceJSiQIDAQABoDUwMwYJKoZIhvcNAQkOMSYwJDAiBgNVHREEGzAZhwR/
|
||||
AAABgRF2YWxpZEBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAf1tnXgX1
|
||||
/1p2MAxHhcil5/lsOMgHWU5dRL6KjK2cepuBpfzlCbxFtvnsj9WHx46f9Q/xbqy+
|
||||
1A2TJIBUWxK+Eji//WJxbDsi7fmV5VQlpG7+sEa7yin3KobfMd84nDIYP8wLF1Fq
|
||||
HhRf7ZjIDh3zTgBosvIIjGEyABrouGYm4Nl409I09MftGXK/5TLJkgm6sxcJCAHG
|
||||
BMm8IFaI0VN5QFIHKvJ/1oQLpLV+gvtR6jAM/99LXc0SXmFn0Jcy/mE/hxJXJigW
|
||||
dDOblgjliJo0rWwHK4gfsgpMbHjJiG70g0XHtTpBW+i/NyuPnc8RYzBIJv+4sks+
|
||||
hWSmn6/IL46qTg==
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -0,0 +1,19 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDATCCAemgAwIBAgIJAMAMmdiZi5G/MA0GCSqGSIb3DQEBCwUAMCwxFDASBgNV
|
||||
BAMMC2V4YW1wbGUuY29tMRQwEgYDVQQLDAtlbmdpbmVlcmluZzAeFw0xODA5MDEx
|
||||
NDM0NTVaFw0yODA4MjkxNDM0NTVaMCwxFDASBgNVBAMMC2V4YW1wbGUuY29tMRQw
|
||||
EgYDVQQLDAtlbmdpbmVlcmluZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBALCOGSVn0JiP96n2nGrOztutCIotOqEz+BqEOUKQGL3H1iip5F2oHdHwZfoX
|
||||
6Z6yuqI2GO9SbXDLLVXX39lnOQ9250PB4pG9UzIXQALWQ1FuEG3lgFyeJpA6tQEU
|
||||
XG07ytdtW5g0KdXn3qaIXtJg8TQq7hkh6an2sguizpJO12e91QDnDz1eTjqWPHpe
|
||||
6rlHX7xJcZ9LxygVZRUQQVtn22ORABwzJJgY1q9KH6cG/r4UrkJTmYLG3dtJow8E
|
||||
mCG8sBtLQBDqI6SyNKTkFeZrHX26pimBOmZlHi/0P6WY2IebsRVGeGjA/0kw2nqu
|
||||
xnWEGHJte1i/jXHmpffoCnHiUokCAwEAAaMmMCQwIgYDVR0RBBswGYcEfwAAAYER
|
||||
dmFsaWRAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAHATSjW20P7+6en0
|
||||
Oq/n/R/i+aCgzcxIWSgf3dhOyxGfBW6svSg8ZtBQFEZZHqIRSXZX89zz25+mvwqi
|
||||
kGRJKKzD/KDd2v9C5+H3DSuu9CqClVtpjF2XLvRHnuclBIrwvyijRcqa2GCTA9YZ
|
||||
sOfVVGQYobDbtRCgTwWkEpU9RrZWWoD8HAYMkxFc1Cs/vJconeAaQDPEIZx9wnAN
|
||||
4r/F5143rn5dyhbYehz1/gykL3K0v7s4U5NhaSACE2AiQ+63vhAEd5xt9WPKAAGY
|
||||
zEyK4b/qPO88mxLr3A/rdzzt1UYAwT38kXA7aV82AH1J8EaCr7tLnXzyLXiEsI4E
|
||||
BOrHBgU=
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwjhklZ9CYj/ep
|
||||
9pxqzs7brQiKLTqhM/gahDlCkBi9x9YoqeRdqB3R8GX6F+mesrqiNhjvUm1wyy1V
|
||||
19/ZZzkPdudDweKRvVMyF0AC1kNRbhBt5YBcniaQOrUBFFxtO8rXbVuYNCnV596m
|
||||
iF7SYPE0Ku4ZIemp9rILos6STtdnvdUA5w89Xk46ljx6Xuq5R1+8SXGfS8coFWUV
|
||||
EEFbZ9tjkQAcMySYGNavSh+nBv6+FK5CU5mCxt3bSaMPBJghvLAbS0AQ6iOksjSk
|
||||
5BXmax19uqYpgTpmZR4v9D+lmNiHm7EVRnhowP9JMNp6rsZ1hBhybXtYv41x5qX3
|
||||
6Apx4lKJAgMBAAECggEAF1Jd7fv9qPlzfKcP2GgDGS+NLjt1QDAOOOp4aduA+Si5
|
||||
mFuAyAJaFg5MWjHocUcosh61Qn+/5yNflLRUZHJnLizFtcSZuiipIbfCg91rvQjt
|
||||
8KZdQ168t1aZ7E+VOfSpAbX3YG6bjB754UOoSt/1XK/DDdzV8dadhD34TYlOmOxZ
|
||||
MMnIRERqa+IBSn90TONWPyY3ELSpaiCkz1YZpp6g9RnTACZKLwzBMSunNO5qbEfH
|
||||
TWlk5o14DZ3zRu5gLT5wy3SGfzm2M+qi8afQq1MT2I6opXj4KU3c64agjNUBYTq7
|
||||
S2YWmw6yrqPzxcg0hOz9H6djCx2oen/UxM2z4uoE1QKBgQDlHIFQcVTWEmxhy5yp
|
||||
uV7Ya5ubx6rW4FnCgh5lJ+wWuSa5TkMuBr30peJn0G6y0I0J1El4o3iwLD/jxwHb
|
||||
BIJTB1z5fBo3K7lhpZLuRFSWe9Mcd/Aj2pFcy5TqaIV9x8bgVAMVOoZAq9muiEog
|
||||
zIWVWrVF6FDuFgRMRegNDej6pwKBgQDFRpNQMscPpH+x6xeS0E8boZKnHyuJUZQZ
|
||||
kfEmnHQuTYmmHS4kXSnJhjODa53YddknTrPrHOvddDDYAaulyyYitPemubYQzBog
|
||||
MyIgaeFSw/eHrcr/8g4QTohRFcI71xnKRmHvQZb8UflFJkqsqil6WZ6FJiC+STcn
|
||||
Qdnhol9fTwKBgQCZtGDw1cdjgqKhjVcB6nG94ZtYjECJvaOaQW8g0AKsT/SxttaN
|
||||
B0ri2XMl0IijgBROttO/knQCRP1r03PkOocwKq1uVprDzpqk7s6++KqC9nlwDOrX
|
||||
Muf4iD/UbuC3vJIop1QWJtgwhNoaJCcPEAbCZ0Nbrfq1b6Hchb2jHGTj2wKBgHJo
|
||||
DpDJEeaBeMi+1SoAgpA8sKcZDY+SbvgxShAhVcNwli5u586Q9OX5XTCPHbhmB+yi
|
||||
2Pa2DBefBaCPv3LkEJa6KpFXTD4Lj+8ymE0B+nmcSpY19O9f+kX8tVOI8d7wTPWg
|
||||
wbUWbbCg/ZXbshzWhj19cdA4H28bWM/8gZY4K2VDAoGBAMYsNhKdu9ON/7vaLijh
|
||||
kai2tQLObYqDV6OAzdYm1gopmTTLcxQ6jP6aQlyw1ie51ms/hFozmNkGkaQGD8pp
|
||||
751Lv3prQz/lDaZeQfKANNN1tpz/QqUOu2di9secMmodxXkwcLzcEKjWPDTuPhcO
|
||||
VODU1hC5oj8yGFInoDLL2B0K
|
||||
-----END PRIVATE KEY-----
|
|
@ -56,6 +56,11 @@ Sets a CA cert and associated parameters in a role name.
|
|||
(https://github.com/ryanuber/go-glob/blob/master/README.md#example). Value is
|
||||
a comma-separated list of URI patterns. Authentication requires at least one
|
||||
URI matching at least one pattern. If not set, defaults to allowing all URIs.
|
||||
- `allowed_organizational_units` `(string: "" or array: [])` - Constrain the
|
||||
Organizational Units (OU) in the client certificate with a [globbed pattern]
|
||||
(https://github.com/ryanuber/go-glob/blob/master/README.md#example). Value is
|
||||
a comma-separated list of OU patterns. Authentication requires at least one
|
||||
OU matching at least one pattern. If not set, defaults to allowing all OUs.
|
||||
- `required_extensions` `(string: "" or array: [])` - Require specific Custom
|
||||
Extension OIDs to exist and match the pattern. Value is a comma separated
|
||||
string or array of `oid:value`. Expects the extension value to be some type
|
||||
|
|
Loading…
Reference in New Issue