From b9e949bf7330fb8b5048171bdc5b144989eeeb06 Mon Sep 17 00:00:00 2001 From: Victor Rodriguez Date: Fri, 20 Dec 2024 20:55:25 +0100 Subject: [PATCH] Support all fields of the name constraints extension when generating CA certificates (#29245) Support all fields of the name constraints extension when generating CA certs. The PKI secrets engine only provided parameter permitted_dns_domains to create the name constraints extension when generating CA certificates. Add the following parameters to provide full support for the extension: * permitted_email_addresses * permitted_ip_ranges * permitted_uri_domains * excluded_dns_domains * excluded_email_addresses * excluded_ip_ranges * excluded_uri_domains Specifying any combination of these parameters will trigger the creation of the name constraints extension as per RFC 5280 section 4.2.1.10. --- builtin/logical/pki/cert_util.go | 56 +++ builtin/logical/pki/cert_util_test.go | 466 +++++++++++------- builtin/logical/pki/fields.go | 54 ++ builtin/logical/pki/issuing/sign_cert.go | 53 ++ changelog/29245.txt | 3 + sdk/helper/certutil/helpers.go | 98 ++-- sdk/helper/certutil/types.go | 11 +- website/content/api-docs/secret/pki/index.mdx | 38 ++ .../docs/commands/pki/health-check.mdx | 7 + .../docs/secrets/pki/considerations.mdx | 18 +- 10 files changed, 587 insertions(+), 217 deletions(-) create mode 100644 changelog/29245.txt diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index ae5d3504d4..8804682122 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -348,6 +348,19 @@ func generateCert(sc *storageContext, if isCA { data.Params.IsCA = isCA data.Params.PermittedDNSDomains = input.apiData.Get("permitted_dns_domains").([]string) + data.Params.ExcludedDNSDomains = input.apiData.Get("excluded_dns_domains").([]string) + data.Params.PermittedIPRanges, err = convertIpRanges(input.apiData.Get("permitted_ip_ranges").([]string)) + if err != nil { + return nil, nil, errutil.UserError{Err: fmt.Sprintf("invalid permitted_ip_ranges value: %s", err)} + } + data.Params.ExcludedIPRanges, err = convertIpRanges(input.apiData.Get("excluded_ip_ranges").([]string)) + if err != nil { + return nil, nil, errutil.UserError{Err: fmt.Sprintf("invalid excluded_ip_ranges value: %s", err)} + } + data.Params.PermittedEmailAddresses = input.apiData.Get("permitted_email_addresses").([]string) + data.Params.ExcludedEmailAddresses = input.apiData.Get("excluded_email_addresses").([]string) + data.Params.PermittedURIDomains = input.apiData.Get("permitted_uri_domains").([]string) + data.Params.ExcludedURIDomains = input.apiData.Get("excluded_uri_domains").([]string) if data.SigningBundle == nil { // Generating a self-signed root certificate. Since we have no @@ -399,6 +412,21 @@ func generateCert(sc *storageContext, return parsedBundle, warnings, nil } +// convertIpRanges parses each string in the input slice as an IP network. Input +// strings are expected to be in the CIDR notation of IP address and prefix length +// like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291. +func convertIpRanges(ipRanges []string) ([]*net.IPNet, error) { + var ret []*net.IPNet + for _, ipRange := range ipRanges { + _, ipnet, err := net.ParseCIDR(ipRange) + if err != nil { + return nil, fmt.Errorf("error parsing IP range %q: %w", ipRange, err) + } + ret = append(ret, ipnet) + } + return ret, nil +} + // N.B.: This is only meant to be used for generating intermediate CAs. // It skips some sanity checks. func generateIntermediateCSR(sc *storageContext, input *inputBundle, randomSource io.Reader) (*certutil.ParsedCSRBundle, []string, error) { @@ -472,6 +500,34 @@ func (i SignCertInputFromDataFields) GetPermittedDomains() []string { return i.data.Get("permitted_dns_domains").([]string) } +func (i SignCertInputFromDataFields) GetExcludedDomains() []string { + return i.data.Get("excluded_dns_domains").([]string) +} + +func (i SignCertInputFromDataFields) GetPermittedIpRanges() ([]*net.IPNet, error) { + return convertIpRanges(i.data.Get("permitted_ip_ranges").([]string)) +} + +func (i SignCertInputFromDataFields) GetExcludedIpRanges() ([]*net.IPNet, error) { + return convertIpRanges(i.data.Get("excluded_ip_ranges").([]string)) +} + +func (i SignCertInputFromDataFields) GetPermittedEmailAddresses() []string { + return i.data.Get("permitted_email_addresses").([]string) +} + +func (i SignCertInputFromDataFields) GetExcludedEmailAddresses() []string { + return i.data.Get("excluded_email_addresses").([]string) +} + +func (i SignCertInputFromDataFields) GetPermittedUriDomains() []string { + return i.data.Get("permitted_uri_domains").([]string) +} + +func (i SignCertInputFromDataFields) GetExcludedUriDomains() []string { + return i.data.Get("excluded_uri_domains").([]string) +} + func (i SignCertInputFromDataFields) IgnoreCSRSignature() bool { return false } diff --git a/builtin/logical/pki/cert_util_test.go b/builtin/logical/pki/cert_util_test.go index 212d5bda70..30f0f71c77 100644 --- a/builtin/logical/pki/cert_util_test.go +++ b/builtin/logical/pki/cert_util_test.go @@ -267,13 +267,13 @@ func TestPki_PermitFQDNs(t *testing.T) { } type parseCertificateTestCase struct { - name string - data map[string]interface{} - roleData map[string]interface{} // if a role is to be created - ttl time.Duration - wantParams certutil.CreationParameters - wantFields map[string]interface{} - wantErr bool + name string + data map[string]interface{} + roleData map[string]interface{} // if a role is to be created + ttl time.Duration + wantParams certutil.CreationParameters + wantFields map[string]interface{} + wantIssuanceErr string // If not empty, require.ErrorContains will be used on this string } // TestDisableVerifyCertificateEnvVar verifies that env var VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION @@ -387,12 +387,16 @@ func TestParseCertificate(t *testing.T) { parseURL := func(s string) *url.URL { u, err := url.Parse(s) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return u } + convertIps := func(ipRanges ...string) []*net.IPNet { + ret, err := convertIpRanges(ipRanges) + require.NoError(t, err) + return ret + } + tests := []*parseCertificateTestCase{ { name: "simple CA", @@ -434,56 +438,69 @@ func TestParseCertificate(t *testing.T) { SKID: []byte("We'll assert that it is not nil as an special case"), }, wantFields: map[string]interface{}{ - "common_name": "the common name", - "alt_names": "", - "ip_sans": "", - "uri_sans": "", - "other_sans": "", - "signature_bits": 384, - "exclude_cn_from_sans": true, - "ou": "", - "organization": "", - "country": "", - "locality": "", - "province": "", - "street_address": "", - "postal_code": "", - "serial_number": "", - "ttl": "1h0m30s", - "max_path_length": -1, - "permitted_dns_domains": "", - "use_pss": false, - "key_type": "ec", - "key_bits": 384, - "skid": "We'll assert that it is not nil as an special case", + "common_name": "the common name", + "alt_names": "", + "ip_sans": "", + "uri_sans": "", + "other_sans": "", + "signature_bits": 384, + "exclude_cn_from_sans": true, + "ou": "", + "organization": "", + "country": "", + "locality": "", + "province": "", + "street_address": "", + "postal_code": "", + "serial_number": "", + "ttl": "1h0m30s", + "max_path_length": -1, + "permitted_dns_domains": "", + "excluded_dns_domains": "", + "permitted_ip_ranges": "", + "excluded_ip_ranges": "", + "permitted_email_addresses": "", + "excluded_email_addresses": "", + "permitted_uri_domains": "", + "excluded_uri_domains": "", + "use_pss": false, + "key_type": "ec", + "key_bits": 384, + "skid": "We'll assert that it is not nil as an special case", }, - wantErr: false, }, { // Note that this test's data is used to create the internal CA used by test "full non CA cert" name: "full CA", data: map[string]interface{}{ // using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#sign-certificate - "common_name": "the common name", - "alt_names": "user@example.com,admin@example.com,example.com,www.example.com", - "ip_sans": "1.2.3.4,1.2.3.5", - "uri_sans": "https://example.com,https://www.example.com", - "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", - "ttl": "2h", - "max_path_length": 2, - "permitted_dns_domains": "example.com,.example.com,.www.example.com", - "ou": "unit1, unit2", - "organization": "org1, org2", - "country": "US, CA", - "locality": "locality1, locality2", - "province": "province1, province2", - "street_address": "street_address1, street_address2", - "postal_code": "postal_code1, postal_code2", - "not_before_duration": "45s", - "key_type": "rsa", - "use_pss": true, - "key_bits": 2048, - "signature_bits": 384, + "common_name": "the common name", + "alt_names": "user@example.com,admin@example.com,example.com,www.example.com", + "ip_sans": "1.2.3.4,1.2.3.5", + "uri_sans": "https://example.com,https://www.example.com", + "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", + "ttl": "2h", + "max_path_length": 2, + "permitted_dns_domains": "example.com,.example.com,.www.example.com", + "excluded_dns_domains": "bad.example.com,reallybad.com", + "permitted_ip_ranges": "192.0.2.1/24,76.76.21.21/24,2001:4860:4860::8889/32", // Note that while an IP address if specified here, it is the network address that will be stored + "excluded_ip_ranges": "127.0.0.1/16,2001:4860:4860::8888/32", + "permitted_email_addresses": "info@example.com,user@example.com,admin@example.com", + "excluded_email_addresses": "root@example.com,robots@example.com", + "permitted_uri_domains": "example.com,www.example.com", + "excluded_uri_domains": "ftp.example.com,gopher.www.example.com", + "ou": "unit1, unit2", + "organization": "org1, org2", + "country": "US, CA", + "locality": "locality1, locality2", + "province": "province1, province2", + "street_address": "street_address1, street_address2", + "postal_code": "postal_code1, postal_code2", + "not_before_duration": "45s", + "key_type": "rsa", + "use_pss": true, + "key_bits": 2048, + "signature_bits": 384, // TODO(kitography): Specify key usage }, ttl: 2 * time.Hour, @@ -517,36 +534,49 @@ func TestParseCertificate(t *testing.T) { ForceAppendCaChain: false, UseCSRValues: false, PermittedDNSDomains: []string{"example.com", ".example.com", ".www.example.com"}, + ExcludedDNSDomains: []string{"bad.example.com", "reallybad.com"}, + PermittedIPRanges: convertIps("192.0.2.0/24", "76.76.21.0/24", "2001:4860::/32"), // Note that we stored the network address rather than the specific IP address + ExcludedIPRanges: convertIps("127.0.0.0/16", "2001:4860::/32"), + PermittedEmailAddresses: []string{"info@example.com", "user@example.com", "admin@example.com"}, + ExcludedEmailAddresses: []string{"root@example.com", "robots@example.com"}, + PermittedURIDomains: []string{"example.com", "www.example.com"}, + ExcludedURIDomains: []string{"ftp.example.com", "gopher.www.example.com"}, URLs: nil, MaxPathLength: 2, NotBeforeDuration: 45 * time.Second, SKID: []byte("We'll assert that it is not nil as an special case"), }, wantFields: map[string]interface{}{ - "common_name": "the common name", - "alt_names": "example.com,www.example.com,admin@example.com,user@example.com", - "ip_sans": "1.2.3.4,1.2.3.5", - "uri_sans": "https://example.com,https://www.example.com", - "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", - "signature_bits": 384, - "exclude_cn_from_sans": true, - "ou": "unit1,unit2", - "organization": "org1,org2", - "country": "CA,US", - "locality": "locality1,locality2", - "province": "province1,province2", - "street_address": "street_address1,street_address2", - "postal_code": "postal_code1,postal_code2", - "serial_number": "", - "ttl": "2h0m45s", - "max_path_length": 2, - "permitted_dns_domains": "example.com,.example.com,.www.example.com", - "use_pss": true, - "key_type": "rsa", - "key_bits": 2048, - "skid": "We'll assert that it is not nil as an special case", + "common_name": "the common name", + "alt_names": "example.com,www.example.com,admin@example.com,user@example.com", + "ip_sans": "1.2.3.4,1.2.3.5", + "uri_sans": "https://example.com,https://www.example.com", + "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", + "signature_bits": 384, + "exclude_cn_from_sans": true, + "ou": "unit1,unit2", + "organization": "org1,org2", + "country": "CA,US", + "locality": "locality1,locality2", + "province": "province1,province2", + "street_address": "street_address1,street_address2", + "postal_code": "postal_code1,postal_code2", + "serial_number": "", + "ttl": "2h0m45s", + "max_path_length": 2, + "permitted_dns_domains": "example.com,.example.com,.www.example.com", + "excluded_dns_domains": "bad.example.com,reallybad.com", + "permitted_ip_ranges": "192.0.2.0/24,76.76.21.0/24,2001:4860::/32", + "excluded_ip_ranges": "127.0.0.0/16,2001:4860::/32", + "permitted_email_addresses": "info@example.com,user@example.com,admin@example.com", + "excluded_email_addresses": "root@example.com,robots@example.com", + "permitted_uri_domains": "example.com,www.example.com", + "excluded_uri_domains": "ftp.example.com,gopher.www.example.com", + "use_pss": true, + "key_type": "rsa", + "key_bits": 2048, + "skid": "We'll assert that it is not nil as an special case", }, - wantErr: false, }, { // Note that we use the data of test "full CA" to create the internal CA needed for this test @@ -555,7 +585,7 @@ func TestParseCertificate(t *testing.T) { // using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#generate-certificate-and-key "common_name": "the common name non ca", "alt_names": "user@example.com,admin@example.com,example.com,www.example.com", - "ip_sans": "1.2.3.4,1.2.3.5", + "ip_sans": "192.0.2.1,192.0.2.2", // These must be permitted by the full CA "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", "ttl": "2h", @@ -589,7 +619,7 @@ func TestParseCertificate(t *testing.T) { }, DNSNames: []string{"example.com", "www.example.com"}, EmailAddresses: []string{"admin@example.com", "user@example.com"}, - IPAddresses: []net.IP{[]byte{1, 2, 3, 4}, []byte{1, 2, 3, 5}}, + IPAddresses: []net.IP{[]byte{192, 0, 2, 1}, []byte{192, 0, 2, 2}}, URIs: []*url.URL{parseURL("https://example.com"), parseURL("https://www.example.com")}, OtherSANs: map[string][]string{"1.3.6.1.4.1.311.20.2.3": {"caadmin@example.com"}}, IsCA: false, @@ -612,30 +642,120 @@ func TestParseCertificate(t *testing.T) { SKID: []byte("We'll assert that it is not nil as an special case"), }, wantFields: map[string]interface{}{ - "common_name": "the common name non ca", - "alt_names": "example.com,www.example.com,admin@example.com,user@example.com", - "ip_sans": "1.2.3.4,1.2.3.5", - "uri_sans": "https://example.com,https://www.example.com", - "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", - "signature_bits": 384, - "exclude_cn_from_sans": true, - "ou": "", - "organization": "", - "country": "", - "locality": "", - "province": "", - "street_address": "", - "postal_code": "", - "serial_number": "", - "ttl": "2h0m45s", - "max_path_length": 0, - "permitted_dns_domains": "", - "use_pss": false, - "key_type": "rsa", - "key_bits": 2048, - "skid": "We'll assert that it is not nil as an special case", + "common_name": "the common name non ca", + "alt_names": "example.com,www.example.com,admin@example.com,user@example.com", + "ip_sans": "192.0.2.1,192.0.2.2", + "uri_sans": "https://example.com,https://www.example.com", + "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", + "signature_bits": 384, + "exclude_cn_from_sans": true, + "ou": "", + "organization": "", + "country": "", + "locality": "", + "province": "", + "street_address": "", + "postal_code": "", + "serial_number": "", + "ttl": "2h0m45s", + "max_path_length": 0, + "permitted_dns_domains": "", + "excluded_dns_domains": "", + "permitted_ip_ranges": "", + "excluded_ip_ranges": "", + "permitted_email_addresses": "", + "excluded_email_addresses": "", + "permitted_uri_domains": "", + "excluded_uri_domains": "", + "use_pss": false, + "key_type": "rsa", + "key_bits": 2048, + "skid": "We'll assert that it is not nil as an special case", }, - wantErr: false, + }, + { + name: "DNS domain not permitted", + data: map[string]interface{}{ + "common_name": "the common name non ca", + "alt_names": "badexample.com", + "ttl": "2h", + }, + ttl: 2 * time.Hour, + roleData: map[string]interface{}{ + "allow_any_name": true, + "cn_validations": "disabled", + }, + wantIssuanceErr: `DNS name "badexample.com" is not permitted by any constraint`, + }, + { + name: "DNS domain explicitly excluded", + data: map[string]interface{}{ + "common_name": "the common name non ca", + "alt_names": "bad.example.com", + "ttl": "2h", + }, + ttl: 2 * time.Hour, + roleData: map[string]interface{}{ + "allow_any_name": true, + "cn_validations": "disabled", + }, + wantIssuanceErr: `DNS name "bad.example.com" is excluded by constraint "bad.example.com"`, + }, + { + name: "IP address not permitted", + data: map[string]interface{}{ + "common_name": "the common name non ca", + "ip_sans": "192.0.3.1", + "ttl": "2h", + }, + ttl: 2 * time.Hour, + roleData: map[string]interface{}{ + "allow_any_name": true, + "cn_validations": "disabled", + }, + wantIssuanceErr: `IP address "192.0.3.1" is not permitted by any constraint`, + }, + { + name: "IP address explicitly excluded", + data: map[string]interface{}{ + "common_name": "the common name non ca", + "ip_sans": "127.0.0.123", + "ttl": "2h", + }, + ttl: 2 * time.Hour, + roleData: map[string]interface{}{ + "allow_any_name": true, + "cn_validations": "disabled", + }, + wantIssuanceErr: `IP address "127.0.0.123" is excluded by constraint "127.0.0.0/16"`, + }, + { + name: "email address not permitted", + data: map[string]interface{}{ + "common_name": "the common name non ca", + "alt_names": "random@example.com", + "ttl": "2h", + }, + ttl: 2 * time.Hour, + roleData: map[string]interface{}{ + "allow_any_name": true, + "cn_validations": "disabled", + }, + wantIssuanceErr: `email address "random@example.com" is not permitted by any constraint`, + }, + { + name: "email address explicitly excluded", + data: map[string]interface{}{ + "common_name": "the common name non ca", + "alt_names": "root@example.com", + "ttl": "2h", + }, + ttl: 2 * time.Hour, + roleData: map[string]interface{}{ + "allow_any_name": true, + "cn_validations": "disabled", + }, + wantIssuanceErr: `email address "root@example.com" is excluded by constraint "root@example.com"`, }, } for _, tt := range tests { @@ -668,15 +788,22 @@ func TestParseCertificate(t *testing.T) { // create the cert resp, err = CBWrite(b, s, "issue/test", tt.data) - require.NoError(t, err) - require.NotNil(t, resp) + if tt.wantIssuanceErr != "" { + require.ErrorContains(t, err, tt.wantIssuanceErr) + } else { + require.NoError(t, err) + require.NotNil(t, resp) - certData := resp.Data["certificate"].(string) - cert, err = parsing.ParseCertificateFromString(certData) - require.NoError(t, err) - require.NotNil(t, cert) + certData := resp.Data["certificate"].(string) + cert, err = parsing.ParseCertificateFromString(certData) + require.NoError(t, err) + require.NotNil(t, cert) + } } + if tt.wantIssuanceErr != "" { + return + } t.Run(tt.name+" parameters", func(t *testing.T) { testParseCertificateToCreationParameters(t, issueTime, tt, cert) }) @@ -690,72 +817,64 @@ func TestParseCertificate(t *testing.T) { func testParseCertificateToCreationParameters(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, cert *x509.Certificate) { params, err := certutil.ParseCertificateToCreationParameters(*cert) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) + require.NoError(t, err) - ignoreBasicConstraintsValidForNonCA := tt.wantParams.IsCA + ignoreBasicConstraintsValidForNonCA := tt.wantParams.IsCA - var diff []string - for _, d := range deep.Equal(tt.wantParams, params) { - switch { - case strings.HasPrefix(d, "SKID"): - continue - case strings.HasPrefix(d, "BasicConstraintsValidForNonCA") && ignoreBasicConstraintsValidForNonCA: - continue - case strings.HasPrefix(d, "NotBeforeDuration"): - continue - case strings.HasPrefix(d, "NotAfter"): - continue - } - diff = append(diff, d) + var diff []string + for _, d := range deep.Equal(tt.wantParams, params) { + switch { + case strings.HasPrefix(d, "SKID"): + continue + case strings.HasPrefix(d, "BasicConstraintsValidForNonCA") && ignoreBasicConstraintsValidForNonCA: + continue + case strings.HasPrefix(d, "NotBeforeDuration"): + continue + case strings.HasPrefix(d, "NotAfter"): + continue } - if diff != nil { - t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.Join(diff, "\n")) - } - - require.NotNil(t, params.SKID) - require.GreaterOrEqual(t, params.NotBeforeDuration, tt.wantParams.NotBeforeDuration, - "NotBeforeDuration want: %s got: %s", tt.wantParams.NotBeforeDuration, params.NotBeforeDuration) - - require.GreaterOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(-1*time.Minute), - "NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter) - require.LessOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(1*time.Minute), - "NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter) + diff = append(diff, d) } + if diff != nil { + t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.Join(diff, "\n")) + } + + require.NotNil(t, params.SKID) + require.GreaterOrEqual(t, params.NotBeforeDuration, tt.wantParams.NotBeforeDuration, + "NotBeforeDuration want: %s got: %s", tt.wantParams.NotBeforeDuration, params.NotBeforeDuration) + + require.GreaterOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(-1*time.Minute), + "NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter) + require.LessOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(1*time.Minute), + "NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter) } func testParseCertificateToFields(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, cert *x509.Certificate) { fields, err := certutil.ParseCertificateToFields(*cert) - if tt.wantErr { - require.Error(t, err) - } else { + require.NoError(t, err) + + require.NotNil(t, fields["skid"]) + delete(fields, "skid") + delete(tt.wantFields, "skid") + + { + // Sometimes TTL comes back as 1s off, so we'll allow that + expectedTTL, err := parseutil.ParseDurationSecond(tt.wantFields["ttl"].(string)) + require.NoError(t, err) + actualTTL, err := parseutil.ParseDurationSecond(fields["ttl"].(string)) require.NoError(t, err) - require.NotNil(t, fields["skid"]) - delete(fields, "skid") - delete(tt.wantFields, "skid") + diff := expectedTTL - actualTTL + require.LessOrEqual(t, actualTTL, expectedTTL, // NotAfter is generated before NotBefore so the time.Now of notBefore may be later, shrinking our calculated TTL during very slow tests + "ttl should be, if off, smaller than expected want: %s got: %s", tt.wantFields["ttl"], fields["ttl"]) + require.LessOrEqual(t, diff, 30*time.Second, // Test can be slow, allow more off in the other direction + "ttl must be at most 30s off, want: %s got: %s", tt.wantFields["ttl"], fields["ttl"]) + delete(fields, "ttl") + delete(tt.wantFields, "ttl") + } - { - // Sometimes TTL comes back as 1s off, so we'll allow that - expectedTTL, err := parseutil.ParseDurationSecond(tt.wantFields["ttl"].(string)) - require.NoError(t, err) - actualTTL, err := parseutil.ParseDurationSecond(fields["ttl"].(string)) - require.NoError(t, err) - - diff := expectedTTL - actualTTL - require.LessOrEqual(t, actualTTL, expectedTTL, // NotAfter is generated before NotBefore so the time.Now of notBefore may be later, shrinking our calculated TTL during very slow tests - "ttl should be, if off, smaller than expected want: %s got: %s", tt.wantFields["ttl"], fields["ttl"]) - require.LessOrEqual(t, diff, 30*time.Second, // Test can be slow, allow more off in the other direction - "ttl must be at most 30s off, want: %s got: %s", tt.wantFields["ttl"], fields["ttl"]) - delete(fields, "ttl") - delete(tt.wantFields, "ttl") - } - - if diff := deep.Equal(tt.wantFields, fields); diff != nil { - t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) - } + if diff := deep.Equal(tt.wantFields, fields); diff != nil { + t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) } } @@ -831,7 +950,6 @@ func TestParseCsr(t *testing.T) { "serial_number": "", "add_basic_constraints": false, }, - wantErr: false, }, { name: "full CSR with basic constraints", @@ -918,7 +1036,6 @@ func TestParseCsr(t *testing.T) { "serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", "add_basic_constraints": true, }, - wantErr: false, }, { name: "full CSR without basic constraints", @@ -1005,7 +1122,6 @@ func TestParseCsr(t *testing.T) { "serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", "add_basic_constraints": false, }, - wantErr: false, }, } for _, tt := range tests { @@ -1034,26 +1150,18 @@ func TestParseCsr(t *testing.T) { func testParseCsrToCreationParameters(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, csr *x509.CertificateRequest) { params, err := certutil.ParseCsrToCreationParameters(*csr) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) + require.NoError(t, err) - if diff := deep.Equal(tt.wantParams, params); diff != nil { - t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) - } + if diff := deep.Equal(tt.wantParams, params); diff != nil { + t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) } } func testParseCsrToFields(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, csr *x509.CertificateRequest) { fields, err := certutil.ParseCsrToFields(*csr) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) + require.NoError(t, err) - if diff := deep.Equal(tt.wantFields, fields); diff != nil { - t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) - } + if diff := deep.Equal(tt.wantFields, fields); diff != nil { + t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) } } diff --git a/builtin/logical/pki/fields.go b/builtin/logical/pki/fields.go index dfeec9df4b..24d030576f 100644 --- a/builtin/logical/pki/fields.go +++ b/builtin/logical/pki/fields.go @@ -390,6 +390,60 @@ func addCAIssueFields(fields map[string]*framework.FieldSchema) map[string]*fram Name: "Permitted DNS Domains", }, } + fields["excluded_dns_domains"] = &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `Domains for which this certificate is not allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Excluded DNS Domains", + }, + } + + fields["permitted_ip_ranges"] = &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `IP ranges for which this certificate is allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10). +Ranges must be specified in the notation of IP address and prefix length, like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291.`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Permitted IP ranges", + }, + } + fields["excluded_ip_ranges"] = &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `IP ranges for which this certificate is not allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10). +Ranges must be specified in the notation of IP address and prefix length, like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291.`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Excluded IP ranges", + }, + } + + fields["permitted_email_addresses"] = &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `Email addresses for which this certificate is allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Permitted email adresses", + }, + } + fields["excluded_email_addresses"] = &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `Email addresses for which this certificate is not allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Excluded email addresses", + }, + } + + fields["permitted_uri_domains"] = &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `URI domains for which this certificate is allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Permitted URI domains", + }, + } + fields["excluded_uri_domains"] = &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `URI domains for which this certificate is not allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`, + DisplayAttrs: &framework.DisplayAttributes{ + Name: "Excluded URI domains", + }, + } fields = addIssuerNameField(fields) diff --git a/builtin/logical/pki/issuing/sign_cert.go b/builtin/logical/pki/issuing/sign_cert.go index c6548f6952..8894f2def8 100644 --- a/builtin/logical/pki/issuing/sign_cert.go +++ b/builtin/logical/pki/issuing/sign_cert.go @@ -9,6 +9,7 @@ import ( "crypto/rsa" "crypto/x509" "fmt" + "net" "github.com/hashicorp/vault/sdk/helper/certutil" "github.com/hashicorp/vault/sdk/helper/errutil" @@ -21,6 +22,13 @@ type SignCertInput interface { IsCA() bool UseCSRValues() bool GetPermittedDomains() []string + GetExcludedDomains() []string + GetPermittedIpRanges() ([]*net.IPNet, error) + GetExcludedIpRanges() ([]*net.IPNet, error) + GetPermittedEmailAddresses() []string + GetExcludedEmailAddresses() []string + GetPermittedUriDomains() []string + GetExcludedUriDomains() []string } func NewBasicSignCertInput(csr *x509.CertificateRequest, isCA, useCSRValues bool) BasicSignCertInput { @@ -113,6 +121,38 @@ func (b BasicSignCertInput) GetPermittedDomains() []string { return []string{} } +func (b BasicSignCertInput) GetExcludedDomains() []string { + return []string{} +} + +// GetPermittedIpRanges returns the permitted IP ranges for the name constraints extension. +// ignore-nil-nil-function-check +func (b BasicSignCertInput) GetPermittedIpRanges() ([]*net.IPNet, error) { + return nil, nil +} + +// GetExcludedIpRanges returns the excluded IP ranges for the name constraints extension. +// ignore-nil-nil-function-check +func (b BasicSignCertInput) GetExcludedIpRanges() ([]*net.IPNet, error) { + return nil, nil +} + +func (b BasicSignCertInput) GetPermittedEmailAddresses() []string { + return []string{} +} + +func (b BasicSignCertInput) GetExcludedEmailAddresses() []string { + return []string{} +} + +func (b BasicSignCertInput) GetPermittedUriDomains() []string { + return []string{} +} + +func (b BasicSignCertInput) GetExcludedUriDomains() []string { + return []string{} +} + func SignCert(b logical.SystemView, role *RoleEntry, entityInfo EntityInfo, caSign *certutil.CAInfoBundle, signInput SignCertInput) (*certutil.ParsedCertBundle, []string, error) { if role == nil { return nil, nil, errutil.InternalError{Err: "no role found in data bundle"} @@ -284,6 +324,19 @@ func SignCert(b logical.SystemView, role *RoleEntry, entityInfo EntityInfo, caSi if signInput.IsCA() { creation.Params.PermittedDNSDomains = signInput.GetPermittedDomains() + creation.Params.ExcludedDNSDomains = signInput.GetExcludedDomains() + creation.Params.PermittedIPRanges, err = signInput.GetPermittedIpRanges() + if err != nil { + return nil, nil, errutil.UserError{Err: fmt.Sprintf("error parsinng permitted IP ranges: %v", err)} + } + creation.Params.ExcludedIPRanges, err = signInput.GetExcludedIpRanges() + if err != nil { + return nil, nil, errutil.UserError{Err: fmt.Sprintf("error parsinng excluded IP ranges: %v", err)} + } + creation.Params.PermittedEmailAddresses = signInput.GetPermittedEmailAddresses() + creation.Params.ExcludedEmailAddresses = signInput.GetExcludedEmailAddresses() + creation.Params.PermittedURIDomains = signInput.GetPermittedUriDomains() + creation.Params.ExcludedURIDomains = signInput.GetExcludedUriDomains() } else { for _, ext := range csr.Extensions { if ext.Id.Equal(certutil.ExtensionBasicConstraintsOID) { diff --git a/changelog/29245.txt b/changelog/29245.txt new file mode 100644 index 0000000000..17d6b0837d --- /dev/null +++ b/changelog/29245.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/pki: Complete the set of name constraints parameters by adding permitted_email_addresses, permitted_ip_ranges, permitted_uri_domains, excluded_dns_domains, excluded_email_addresses, excluded_ip_ranges, and excluded_uri_domains; this makes it possible for the name constraints extension to be fully specified when creating root and intermediate CA certificates. +``` diff --git a/sdk/helper/certutil/helpers.go b/sdk/helper/certutil/helpers.go index 7f34da6168..2bfa302a1f 100644 --- a/sdk/helper/certutil/helpers.go +++ b/sdk/helper/certutil/helpers.go @@ -939,10 +939,18 @@ func createCertificate(data *CreationBundle, randReader io.Reader, privateKeyGen } // This will only be filled in from the generation paths - if len(data.Params.PermittedDNSDomains) > 0 { - certTemplate.PermittedDNSDomains = data.Params.PermittedDNSDomains - certTemplate.PermittedDNSDomainsCritical = true - } + certTemplate.PermittedDNSDomains = append(certTemplate.PermittedDNSDomains, data.Params.PermittedDNSDomains...) + certTemplate.ExcludedDNSDomains = append(certTemplate.ExcludedDNSDomains, data.Params.ExcludedDNSDomains...) + certTemplate.PermittedIPRanges = append(certTemplate.PermittedIPRanges, data.Params.PermittedIPRanges...) + certTemplate.ExcludedIPRanges = append(certTemplate.ExcludedIPRanges, data.Params.ExcludedIPRanges...) + certTemplate.PermittedEmailAddresses = append(certTemplate.PermittedEmailAddresses, data.Params.PermittedEmailAddresses...) + certTemplate.ExcludedEmailAddresses = append(certTemplate.ExcludedEmailAddresses, data.Params.ExcludedEmailAddresses...) + certTemplate.PermittedURIDomains = append(certTemplate.PermittedURIDomains, data.Params.PermittedURIDomains...) + certTemplate.ExcludedURIDomains = append(certTemplate.ExcludedURIDomains, data.Params.ExcludedURIDomains...) + // Note that it is harmless to set PermittedDNSDomainsCritical even if all other + // permitted or excluded fields are empty, as the name constraints extension won't be created in + // that case + certTemplate.PermittedDNSDomainsCritical = true AddPolicyIdentifiers(data, certTemplate) @@ -1352,10 +1360,15 @@ func signCertificate(data *CreationBundle, randReader io.Reader) (*ParsedCertBun certTemplate.IsCA = false } - if len(data.Params.PermittedDNSDomains) > 0 { - certTemplate.PermittedDNSDomains = data.Params.PermittedDNSDomains - certTemplate.PermittedDNSDomainsCritical = true - } + certTemplate.ExcludedDNSDomains = append(certTemplate.ExcludedDNSDomains, data.Params.ExcludedDNSDomains...) + certTemplate.PermittedIPRanges = append(certTemplate.PermittedIPRanges, data.Params.PermittedIPRanges...) + certTemplate.ExcludedIPRanges = append(certTemplate.ExcludedIPRanges, data.Params.ExcludedIPRanges...) + certTemplate.PermittedEmailAddresses = append(certTemplate.PermittedEmailAddresses, data.Params.PermittedEmailAddresses...) + certTemplate.ExcludedEmailAddresses = append(certTemplate.ExcludedEmailAddresses, data.Params.ExcludedEmailAddresses...) + certTemplate.PermittedURIDomains = append(certTemplate.PermittedURIDomains, data.Params.PermittedURIDomains...) + certTemplate.ExcludedURIDomains = append(certTemplate.ExcludedURIDomains, data.Params.ExcludedURIDomains...) + // Note that it is harmless to set PermittedDNSDomainsCritical even if all other permitted/excluded fields are empty + certTemplate.PermittedDNSDomainsCritical = true certBytes, err = x509.CreateCertificate(randReader, certTemplate, caCert, data.CSR.PublicKey, data.SigningBundle.PrivateKey) if err != nil { @@ -1792,7 +1805,15 @@ func ParseCertificateToCreationParameters(certificate x509.Certificate) (creatio // The following two values are on creation parameters, but are impossible to parse from the certificate // ForceAppendCaChain // UseCSRValues - PermittedDNSDomains: certificate.PermittedDNSDomains, + PermittedDNSDomains: certificate.PermittedDNSDomains, + ExcludedDNSDomains: certificate.ExcludedDNSDomains, + PermittedIPRanges: certificate.PermittedIPRanges, + ExcludedIPRanges: certificate.ExcludedIPRanges, + PermittedEmailAddresses: certificate.PermittedEmailAddresses, + ExcludedEmailAddresses: certificate.ExcludedEmailAddresses, + PermittedURIDomains: certificate.PermittedURIDomains, + ExcludedURIDomains: certificate.ExcludedURIDomains, + // URLs: punting on this for now MaxPathLength: certificate.MaxPathLen, NotBeforeDuration: time.Now().Sub(certificate.NotBefore), // Assumes Certificate was created this moment @@ -1934,33 +1955,48 @@ func ParseCertificateToFields(certificate x509.Certificate) (map[string]interfac } templateData := map[string]interface{}{ - "common_name": certificate.Subject.CommonName, - "alt_names": MakeAltNamesCommaSeparatedString(certificate.DNSNames, certificate.EmailAddresses), - "ip_sans": MakeIpAddressCommaSeparatedString(certificate.IPAddresses), - "uri_sans": MakeUriCommaSeparatedString(certificate.URIs), - "other_sans": otherSans, - "signature_bits": FindSignatureBits(certificate.SignatureAlgorithm), - "exclude_cn_from_sans": DetermineExcludeCnFromCertSans(certificate), - "ou": makeCommaSeparatedString(certificate.Subject.OrganizationalUnit), - "organization": makeCommaSeparatedString(certificate.Subject.Organization), - "country": makeCommaSeparatedString(certificate.Subject.Country), - "locality": makeCommaSeparatedString(certificate.Subject.Locality), - "province": makeCommaSeparatedString(certificate.Subject.Province), - "street_address": makeCommaSeparatedString(certificate.Subject.StreetAddress), - "postal_code": makeCommaSeparatedString(certificate.Subject.PostalCode), - "serial_number": certificate.Subject.SerialNumber, - "ttl": (certificate.NotAfter.Sub(certificate.NotBefore)).String(), - "max_path_length": certificate.MaxPathLen, - "permitted_dns_domains": strings.Join(certificate.PermittedDNSDomains, ","), - "use_pss": IsPSS(certificate.SignatureAlgorithm), - "skid": hex.EncodeToString(certificate.SubjectKeyId), - "key_type": GetKeyType(certificate.PublicKeyAlgorithm.String()), - "key_bits": FindBitLength(certificate.PublicKey), + "common_name": certificate.Subject.CommonName, + "alt_names": MakeAltNamesCommaSeparatedString(certificate.DNSNames, certificate.EmailAddresses), + "ip_sans": MakeIpAddressCommaSeparatedString(certificate.IPAddresses), + "uri_sans": MakeUriCommaSeparatedString(certificate.URIs), + "other_sans": otherSans, + "signature_bits": FindSignatureBits(certificate.SignatureAlgorithm), + "exclude_cn_from_sans": DetermineExcludeCnFromCertSans(certificate), + "ou": makeCommaSeparatedString(certificate.Subject.OrganizationalUnit), + "organization": makeCommaSeparatedString(certificate.Subject.Organization), + "country": makeCommaSeparatedString(certificate.Subject.Country), + "locality": makeCommaSeparatedString(certificate.Subject.Locality), + "province": makeCommaSeparatedString(certificate.Subject.Province), + "street_address": makeCommaSeparatedString(certificate.Subject.StreetAddress), + "postal_code": makeCommaSeparatedString(certificate.Subject.PostalCode), + "serial_number": certificate.Subject.SerialNumber, + "ttl": (certificate.NotAfter.Sub(certificate.NotBefore)).String(), + "max_path_length": certificate.MaxPathLen, + "permitted_dns_domains": strings.Join(certificate.PermittedDNSDomains, ","), + "excluded_dns_domains": strings.Join(certificate.ExcludedDNSDomains, ","), + "permitted_ip_ranges": strings.Join(ipRangesToStrings(certificate.PermittedIPRanges), ","), + "excluded_ip_ranges": strings.Join(ipRangesToStrings(certificate.ExcludedIPRanges), ","), + "permitted_email_addresses": strings.Join(certificate.PermittedEmailAddresses, ","), + "excluded_email_addresses": strings.Join(certificate.ExcludedEmailAddresses, ","), + "permitted_uri_domains": strings.Join(certificate.PermittedURIDomains, ","), + "excluded_uri_domains": strings.Join(certificate.ExcludedURIDomains, ","), + "use_pss": IsPSS(certificate.SignatureAlgorithm), + "skid": hex.EncodeToString(certificate.SubjectKeyId), + "key_type": GetKeyType(certificate.PublicKeyAlgorithm.String()), + "key_bits": FindBitLength(certificate.PublicKey), } return templateData, nil } +func ipRangesToStrings(ipRanges []*net.IPNet) []string { + var ret []string + for _, ipRange := range ipRanges { + ret = append(ret, ipRange.String()) + } + return ret +} + func getBasicConstraintsFromExtension(exts []pkix.Extension) (found bool, isCA bool, maxPathLength int, err error) { for _, ext := range exts { if ext.Id.Equal(ExtensionBasicConstraintsOID) { diff --git a/sdk/helper/certutil/types.go b/sdk/helper/certutil/types.go index 4f16659c1d..398a58e210 100644 --- a/sdk/helper/certutil/types.go +++ b/sdk/helper/certutil/types.go @@ -807,8 +807,15 @@ type CreationParameters struct { ForceAppendCaChain bool // Only used when signing a CA cert - UseCSRValues bool - PermittedDNSDomains []string + UseCSRValues bool + PermittedDNSDomains []string + ExcludedDNSDomains []string + PermittedIPRanges []*net.IPNet + ExcludedIPRanges []*net.IPNet + PermittedEmailAddresses []string + ExcludedEmailAddresses []string + PermittedURIDomains []string + ExcludedURIDomains []string // URLs to encode into the certificate URLs *URLEntries diff --git a/website/content/api-docs/secret/pki/index.mdx b/website/content/api-docs/secret/pki/index.mdx index e76829bfb1..abe7c34af2 100644 --- a/website/content/api-docs/secret/pki/index.mdx +++ b/website/content/api-docs/secret/pki/index.mdx @@ -774,6 +774,44 @@ intermediary goes beyond the prescribed length. the domain, as per [RFC 5280 Section 4.2.1.10 - Name Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10) +- `excluded_dns_domains` `(string: "")` - A comma separated string (or, string + array) containing DNS domains for which certificates are not allowed to be issued + or signed by this CA certificate. Supports subdomains via a `.` in front of + the domain, as per [RFC 5280 Section 4.2.1.10 - Name + Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10) + +- `permitted_ip_ranges` `(string: "")` - A comma separated string (or, string + array) containing IP ranges for which certificates are allowed to be issued or + signed by this CA certificate. IP ranges must be in the CIDR notation of IP + address and prefix length like "192.0.2.0/24" or "2001:db8::/32", as defined + in RFC 4632 and RFC 4291. + +- `excluded_ip_ranges` `(string: "")` - A comma separated string (or, string + array) containing IP ranges for which certificates are not allowed to be + issued or signed by this CA certificate. IP ranges must be in the CIDR + notation of IP address and prefix length like "192.0.2.0/24" or + "2001:db8::/32", as defined in RFC 4632 and RFC 4291. + +- `permitted_email_addresses` `(string: "")` - A comma separated string (or, string + array) containing email addresses for which certificates are allowed to be issued or + signed by this CA certificate. + +- `excluded_email_addresses` `(string: "")` - A comma separated string (or, + string array) containing email addresses for which certificates are not + allowed to be issued or signed by this CA certificate. + +- `permitted_uri_domains` `(string: "")` - A comma separated string (or, string + array) containing fully qualified domain names for which certificates are + allowed to be issued or signed by this CA certificate. Supports subdomains via + a `.` in front of the domain, as per [RFC 5280 Section 4.2.1.10 - Name + Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10) + +- `excluded_uri_domains` `(string: "")` - A comma separated string (or, string + array) containing fully qualified domain names for which certificates are not + allowed to be issued or signed by this CA certificate. Supports subdomains via + a `.` in front of the domain, as per [RFC 5280 Section 4.2.1.10 - Name + Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10) + - `ou` `(string: "")` - Specifies the OU (OrganizationalUnit) values in the subject field of the resulting certificate. This is a comma-separated string or JSON array. diff --git a/website/content/docs/commands/pki/health-check.mdx b/website/content/docs/commands/pki/health-check.mdx index 568a9d4078..4b237db091 100644 --- a/website/content/docs/commands/pki/health-check.mdx +++ b/website/content/docs/commands/pki/health-check.mdx @@ -346,6 +346,13 @@ $ vault secrets tune \ -audit-non-hmac-request-keys=street_address \ -audit-non-hmac-request-keys=postal_code \ -audit-non-hmac-request-keys=permitted_dns_domains \ + -audit-non-hmac-request-keys=permitted_email_addresses \ + -audit-non-hmac-request-keys=permitted_ip_ranges \ + -audit-non-hmac-request-keys=permitted_uri_domains \ + -audit-non-hmac-request-keys=excluded_dns_domains \ + -audit-non-hmac-request-keys=excluded_email_addresses \ + -audit-non-hmac-request-keys=excluded_ip_ranges \ + -audit-non-hmac-request-keys=excluded_uri_domains \ -audit-non-hmac-request-keys=policy_identifiers \ -audit-non-hmac-request-keys=ext_key_usage_oids \ -audit-non-hmac-request-keys=csr \ diff --git a/website/content/docs/secrets/pki/considerations.mdx b/website/content/docs/secrets/pki/considerations.mdx index 4d57eeb3be..abe4175f8f 100644 --- a/website/content/docs/secrets/pki/considerations.mdx +++ b/website/content/docs/secrets/pki/considerations.mdx @@ -2,7 +2,7 @@ layout: docs page_title: 'PKI secrets engine considerations' description: >- - Understand the important considerations and guidance before using the PKI secrets engine to generate certificates before using the PKI secrets engine. + Understand the important considerations and guidance before using the PKI secrets engine to generate certificates before using the PKI secrets engine. --- # PKI secrets engine considerations @@ -223,10 +223,11 @@ performance of easier-to-rotate intermediates and certificates (such as TLS intermediates). Vault supports the use of both the [`allowed_domains` parameter on -Roles](/vault/api-docs/secret/pki#allowed_domains) and the [`permitted_dns_domains` -parameter to set the Name Constraints extension](/vault/api-docs/secret/pki#permitted_dns_domains) -on root and intermediate generation. This allows for several layers of -separation of concerns between TLS-based services. +Roles](/vault/api-docs/secret/pki#allowed_domains) and the sef of parameters for +permitted and excluded DNS domains, IP ranges, email addresses and URI domains +to set the [Name Constraints extension](/vault/api-docs/secret/pki#permitted_dns_domains) +on root and intermediate generation. This allows for several layers of separation of +concerns between TLS-based services. ### Cross-Signed intermediates @@ -780,6 +781,13 @@ Some suggested keys to un-HMAC for requests are as follows: - `street_address` - the subject's street address, - `postal_code` - the subject's postal code, - `permitted_dns_domains` - permitted DNS domains, + - `permitted_ip_ranges` - permitted IP ranges, + - `permitted_email_addresses` - permitted email addresses, + - `permitted_uri_domains` - permitted URI domains, + - `excluded_dns_domains` - excluded DNS domains, + - `excluded_email_addresses` - excluded email addresses, + - `excluded_ip_ranges` - excluded IP ranges, + - `excluded_uri_domains` - excluded URI domains, - `policy_identifiers` - the requested policy identifiers when creating a role, and - `ext_key_usage_oids` - the extended key usage OIDs for the requested certificate.