Add allowed_uri_sans_template (#10249)
* Add allowed_uri_sans_template Enables identity templating for the allowed_uri_sans field in PKI cert roles. Implemented as suggested in #8509 * changelog++ * Update docs with URI SAN templating
This commit is contained in:
parent
cbdea53dd7
commit
ccc1098ea3
|
@ -2989,6 +2989,128 @@ func TestBackend_URI_SANs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBackend_AllowedURISANsTemplate(t *testing.T) {
|
||||
coreConfig := &vault.CoreConfig{
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"userpass": userpass.Factory,
|
||||
},
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
"pki": Factory,
|
||||
},
|
||||
}
|
||||
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Write test policy for userpass auth method.
|
||||
err := client.Sys().PutPolicy("test", `
|
||||
path "pki/*" {
|
||||
capabilities = ["update"]
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Enable userpass auth method.
|
||||
if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Configure test role for userpass.
|
||||
if _, err := client.Logical().Write("auth/userpass/users/userpassname", map[string]interface{}{
|
||||
"password": "test",
|
||||
"policies": "test",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Login userpass for test role and keep client token.
|
||||
secret, err := client.Logical().Write("auth/userpass/login/userpassname", map[string]interface{}{
|
||||
"password": "test",
|
||||
})
|
||||
if err != nil || secret == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
userpassToken := secret.Auth.ClientToken
|
||||
|
||||
// Get auth accessor for identity template.
|
||||
auths, err := client.Sys().ListAuth()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
userpassAccessor := auths["userpass/"].Accessor
|
||||
|
||||
// Mount PKI.
|
||||
err = client.Sys().Mount("pki", &api.MountInput{
|
||||
Type: "pki",
|
||||
Config: api.MountConfigInput{
|
||||
DefaultLeaseTTL: "16h",
|
||||
MaxLeaseTTL: "60h",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate internal CA.
|
||||
_, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
|
||||
"ttl": "40h",
|
||||
"common_name": "myvault.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write role PKI.
|
||||
_, err = client.Logical().Write("pki/roles/test", map[string]interface{}{
|
||||
"allowed_uri_sans": []string{"spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}",
|
||||
"spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}/*", "spiffe://domain/foo"},
|
||||
"allowed_uri_sans_template": true,
|
||||
"require_cn": false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Issue certificate with identity templating
|
||||
client.SetToken(userpassToken)
|
||||
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname, spiffe://domain/foo"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Issue certificate with identity templating and glob
|
||||
client.SetToken(userpassToken)
|
||||
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname/bar"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Issue certificate with non-matching identity template parameter
|
||||
client.SetToken(userpassToken)
|
||||
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/unknownuser"})
|
||||
if err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set allowed_uri_sans_template to false.
|
||||
_, err = client.Logical().Write("pki/roles/test", map[string]interface{}{
|
||||
"allowed_uri_sans_template": false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Issue certificate with userpassToken.
|
||||
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/users/userpassname"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackend_AllowedDomainsTemplate(t *testing.T) {
|
||||
coreConfig := &vault.CoreConfig{
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
|
|
|
@ -176,6 +176,29 @@ func fetchCertBySerial(ctx context.Context, req *logical.Request, prefix, serial
|
|||
return certEntry, nil
|
||||
}
|
||||
|
||||
// Given a URI SAN, verify that it is allowed.
|
||||
func validateURISAN(b *backend, data *inputBundle, uri string) bool {
|
||||
valid := false
|
||||
for _, allowed := range data.role.AllowedURISANs {
|
||||
if data.role.AllowedURISANsTemplate {
|
||||
isTemplate, _ := framework.ValidateIdentityTemplate(allowed)
|
||||
if isTemplate && data.req.EntityID != "" {
|
||||
tmpAllowed, err := framework.PopulateIdentityTemplate(allowed, data.req.EntityID, b.System())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
allowed = tmpAllowed
|
||||
}
|
||||
}
|
||||
validURI := glob.Glob(allowed, uri)
|
||||
if validURI {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
// Given a set of requested names for a certificate, verifies that all of them
|
||||
// match the various toggles set in the role for controlling issuance.
|
||||
// If one does not pass, it is returned in the string argument.
|
||||
|
@ -956,15 +979,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
|
|||
|
||||
// validate uri sans
|
||||
for _, uri := range csr.URIs {
|
||||
valid := false
|
||||
for _, allowed := range data.role.AllowedURISANs {
|
||||
validURI := glob.Glob(allowed, uri.String())
|
||||
if validURI {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
valid := validateURISAN(b, data, uri.String())
|
||||
if !valid {
|
||||
return nil, errutil.UserError{
|
||||
Err: fmt.Sprintf(
|
||||
|
@ -986,15 +1001,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
|
|||
}
|
||||
|
||||
for _, uri := range uriAlt {
|
||||
valid := false
|
||||
for _, allowed := range data.role.AllowedURISANs {
|
||||
validURI := glob.Glob(allowed, uri)
|
||||
if validURI {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
valid := validateURISAN(b, data, uri)
|
||||
if !valid {
|
||||
return nil, errutil.UserError{
|
||||
Err: fmt.Sprintf(
|
||||
|
|
|
@ -144,7 +144,14 @@ Any valid URI is accepted, these values support globbing.`,
|
|||
},
|
||||
},
|
||||
|
||||
"allowed_other_sans": {
|
||||
"allowed_uri_sans_template": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Description: `If set, Allowed URI SANs can be specified using identity template policies.
|
||||
Non-templated URI SANs are also permitted.`,
|
||||
Default: false,
|
||||
},
|
||||
|
||||
"allowed_other_sans": &framework.FieldSchema{
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `If set, an array of allowed other names to put in SANs. These values support globbing and must be in the format <oid>;<type>:<value>. Currently only "utf8" is a valid type. All values, including globbing values, must use this syntax, with the exception being a single "*" which allows any OID and any value (but type must still be utf8).`,
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
|
@ -565,6 +572,7 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data
|
|||
AllowSubdomains: data.Get("allow_subdomains").(bool),
|
||||
AllowGlobDomains: data.Get("allow_glob_domains").(bool),
|
||||
AllowAnyName: data.Get("allow_any_name").(bool),
|
||||
AllowedURISANsTemplate: data.Get("allowed_uri_sans_template").(bool),
|
||||
EnforceHostnames: data.Get("enforce_hostnames").(bool),
|
||||
AllowIPSANs: data.Get("allow_ip_sans").(bool),
|
||||
AllowedURISANs: data.Get("allowed_uri_sans").([]string),
|
||||
|
@ -783,6 +791,7 @@ type roleEntry struct {
|
|||
AllowedOtherSANs []string `json:"allowed_other_sans" mapstructure:"allowed_other_sans"`
|
||||
AllowedSerialNumbers []string `json:"allowed_serial_numbers" mapstructure:"allowed_serial_numbers"`
|
||||
AllowedURISANs []string `json:"allowed_uri_sans" mapstructure:"allowed_uri_sans"`
|
||||
AllowedURISANsTemplate bool `json:"allowed_uri_sans_template"`
|
||||
PolicyIdentifiers []string `json:"policy_identifiers" mapstructure:"policy_identifiers"`
|
||||
ExtKeyUsageOIDs []string `json:"ext_key_usage_oids" mapstructure:"ext_key_usage_oids"`
|
||||
BasicConstraintsValidForNonCA bool `json:"basic_constraints_valid_for_non_ca" mapstructure:"basic_constraints_valid_for_non_ca"`
|
||||
|
@ -804,6 +813,7 @@ func (r *roleEntry) ToResponseData() map[string]interface{} {
|
|||
"allow_subdomains": r.AllowSubdomains,
|
||||
"allow_glob_domains": r.AllowGlobDomains,
|
||||
"allow_any_name": r.AllowAnyName,
|
||||
"allowed_uri_sans_template": r.AllowedURISANsTemplate,
|
||||
"enforce_hostnames": r.EnforceHostnames,
|
||||
"allow_ip_sans": r.AllowIPSANs,
|
||||
"server_flag": r.ServerFlag,
|
||||
|
|
|
@ -592,6 +592,12 @@ func TestPki_RoleNoStore(t *testing.T) {
|
|||
t.Fatalf("allowed_domains_template should not be set by default")
|
||||
}
|
||||
|
||||
// By default, allowed_uri_sans_template should be `false`
|
||||
allowedURISANsTemplate := resp.Data["allowed_uri_sans_template"].(bool)
|
||||
if allowedURISANsTemplate {
|
||||
t.Fatalf("allowed_uri_sans_template should not be set by default")
|
||||
}
|
||||
|
||||
// Make sure that setting no_store to `true` works properly
|
||||
roleReq.Operation = logical.UpdateOperation
|
||||
roleReq.Path = "roles/testrole_nostore"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
secrets/pki: Allow URI SAN templates in allowed_uri_sans when allowed_uri_sans_template is set to true.
|
||||
```
|
|
@ -793,6 +793,9 @@ request is denied.
|
|||
a JSON string slice. Values can contain glob patterns (e.g.
|
||||
`spiffe://hostname/*`).
|
||||
|
||||
- `allowed_uri_sans_template` `()bool: false)` – When set, `allowed_uri_sans`
|
||||
may contain templates, as with [ACL Path Templating](/docs/concepts/policies).
|
||||
|
||||
- `allowed_other_sans` `(string: "")` – Defines allowed custom OID/UTF8-string
|
||||
SANs. This can be a comma-delimited list or a JSON string slice, where
|
||||
each element has the same format as OpenSSL: `<oid>;<type>:<value>`, but
|
||||
|
|
Loading…
Reference in New Issue