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:
joe miller 2018-09-27 17:04:55 -07:00 committed by Chris Hoffman
parent dbae477cca
commit d39ffc9e25
8 changed files with 204 additions and 56 deletions

View File

@ -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 {

View File

@ -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 = `

View File

@ -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 {

View File

@ -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

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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