fix bug with allowed_users_template and add allowed_domains_template for SSH role (#16056)

* impr(ssh): fix bug with allowed_users_template and add allowed_domains_template field in SSH role configuration, closes #10943

* chore: add changelog entry
This commit is contained in:
Jakob Beckmann 2022-08-16 21:59:29 +02:00 committed by GitHub
parent dc4965f27d
commit 21a10e09b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 71 additions and 17 deletions

View File

@ -322,6 +322,31 @@ func TestBackend_AllowedUsers(t *testing.T) {
} }
} }
func TestBackend_AllowedDomainsTemplate(t *testing.T) {
testAllowedDomainsTemplate := "{{ identity.entity.metadata.ssh_username }}.example.com"
expectedValidPrincipal := "foo." + testUserName + ".example.com"
testAllowedPrincipalsTemplate(
t, testAllowedDomainsTemplate,
expectedValidPrincipal,
map[string]string{
"ssh_username": testUserName,
},
map[string]interface{}{
"key_type": testCaKeyType,
"algorithm_signer": "rsa-sha2-256",
"allow_host_certificates": true,
"allow_subdomains": true,
"allowed_domains": testAllowedDomainsTemplate,
"allowed_domains_template": true,
},
map[string]interface{}{
"cert_type": "host",
"public_key": testCAPublicKey,
"valid_principals": expectedValidPrincipal,
},
)
}
func TestBackend_AllowedUsersTemplate(t *testing.T) { func TestBackend_AllowedUsersTemplate(t *testing.T) {
testAllowedUsersTemplate(t, testAllowedUsersTemplate(t,
"{{ identity.entity.metadata.ssh_username }}", "{{ identity.entity.metadata.ssh_username }}",
@ -2093,9 +2118,9 @@ func testDefaultUserTemplate(t *testing.T, testDefaultUserTemplate string,
} }
} }
func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string, func testAllowedPrincipalsTemplate(t *testing.T, testAllowedDomainsTemplate string,
expectedValidPrincipal string, testEntityMetadata map[string]string, expectedValidPrincipal string, testEntityMetadata map[string]string,
) { roleConfigPayload map[string]interface{}, signingPayload map[string]interface{}) {
cluster, userpassToken := getSshCaTestCluster(t, testUserName) cluster, userpassToken := getSshCaTestCluster(t, testUserName)
defer cluster.Cleanup() defer cluster.Cleanup()
client := cluster.Cores[0].Client client := cluster.Cores[0].Client
@ -2115,22 +2140,14 @@ func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
t.Fatal(err) t.Fatal(err)
} }
_, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{ _, err = client.Logical().Write("ssh/roles/my-role", roleConfigPayload)
"key_type": testCaKeyType,
"allow_user_certificates": true,
"allowed_users": testAllowedUsersTemplate,
"allowed_users_template": true,
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// sign SSH key as userpass user // sign SSH key as userpass user
client.SetToken(userpassToken) client.SetToken(userpassToken)
signResponse, err := client.Logical().Write("ssh/sign/my-role", map[string]interface{}{ signResponse, err := client.Logical().Write("ssh/sign/my-role", signingPayload)
"public_key": testCAPublicKey,
"valid_principals": expectedValidPrincipal,
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -2151,6 +2168,24 @@ func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
} }
} }
func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
expectedValidPrincipal string, testEntityMetadata map[string]string) {
testAllowedPrincipalsTemplate(
t, testAllowedUsersTemplate,
expectedValidPrincipal, testEntityMetadata,
map[string]interface{}{
"key_type": testCaKeyType,
"allow_user_certificates": true,
"allowed_users": testAllowedUsersTemplate,
"allowed_users_template": true,
},
map[string]interface{}{
"public_key": testCAPublicKey,
"valid_principals": expectedValidPrincipal,
},
)
}
func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep { func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep {
return logicaltest.TestStep{ return logicaltest.TestStep{
Operation: logical.UpdateOperation, Operation: logical.UpdateOperation,

View File

@ -65,7 +65,7 @@ func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logic
var parsedPrincipals []string var parsedPrincipals []string
if certificateType == ssh.HostCert { if certificateType == ssh.HostCert {
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, "", role.AllowedDomains, validateValidPrincipalForHosts(role)) parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, "", role.AllowedDomains, role.AllowedDomainsTemplate, validateValidPrincipalForHosts(role))
if err != nil { if err != nil {
return logical.ErrorResponse(err.Error()), nil return logical.ErrorResponse(err.Error()), nil
} }
@ -77,7 +77,7 @@ func (b *backend) pathSignIssueCertificateHelper(ctx context.Context, req *logic
return nil, err return nil, err
} }
} }
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, defaultPrincipal, role.AllowedUsers, strutil.StrListContains) parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, defaultPrincipal, role.AllowedUsers, role.AllowedUsersTemplate, strutil.StrListContains)
if err != nil { if err != nil {
return logical.ErrorResponse(err.Error()), nil return logical.ErrorResponse(err.Error()), nil
} }
@ -160,7 +160,7 @@ func (b *backend) renderPrincipal(principal string, req *logical.Request) (strin
return principal, nil return principal, nil
} }
func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logical.Request, role *sshRole, defaultPrincipal, principalsAllowedByRole string, validatePrincipal func([]string, string) bool) ([]string, error) { func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logical.Request, role *sshRole, defaultPrincipal, principalsAllowedByRole string, enableTemplating bool, validatePrincipal func([]string, string) bool) ([]string, error) {
validPrincipals := "" validPrincipals := ""
validPrincipalsRaw, ok := data.GetOk("valid_principals") validPrincipalsRaw, ok := data.GetOk("valid_principals")
if ok { if ok {
@ -173,7 +173,7 @@ func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logic
// Build list of allowed Principals from template and static principalsAllowedByRole // Build list of allowed Principals from template and static principalsAllowedByRole
var allowedPrincipals []string var allowedPrincipals []string
for _, principal := range strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) { for _, principal := range strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) {
if role.AllowedUsersTemplate { if enableTemplating {
rendered, err := b.renderPrincipal(principal, req) rendered, err := b.renderPrincipal(principal, req)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -48,6 +48,7 @@ type sshRole struct {
AllowedUsers string `mapstructure:"allowed_users" json:"allowed_users"` AllowedUsers string `mapstructure:"allowed_users" json:"allowed_users"`
AllowedUsersTemplate bool `mapstructure:"allowed_users_template" json:"allowed_users_template"` AllowedUsersTemplate bool `mapstructure:"allowed_users_template" json:"allowed_users_template"`
AllowedDomains string `mapstructure:"allowed_domains" json:"allowed_domains"` AllowedDomains string `mapstructure:"allowed_domains" json:"allowed_domains"`
AllowedDomainsTemplate bool `mapstructure:"allowed_domains_template" json:"allowed_domains_template"`
KeyOptionSpecs string `mapstructure:"key_option_specs" json:"key_option_specs"` KeyOptionSpecs string `mapstructure:"key_option_specs" json:"key_option_specs"`
MaxTTL string `mapstructure:"max_ttl" json:"max_ttl"` MaxTTL string `mapstructure:"max_ttl" json:"max_ttl"`
TTL string `mapstructure:"ttl" json:"ttl"` TTL string `mapstructure:"ttl" json:"ttl"`
@ -223,6 +224,15 @@ func pathRoles(b *backend) *framework.Path {
valid host. If only certain domains are allowed, then this list enforces it. valid host. If only certain domains are allowed, then this list enforces it.
`, `,
}, },
"allowed_domains_template": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
If set, Allowed domains can be specified using identity template policies.
Non-templated domains are also permitted.
`,
Default: false,
},
"key_option_specs": { "key_option_specs": {
Type: framework.TypeString, Type: framework.TypeString,
Description: ` Description: `
@ -567,6 +577,7 @@ func (b *backend) createCARole(allowedUsers, defaultUser, signer string, data *f
AllowedUsers: allowedUsers, AllowedUsers: allowedUsers,
AllowedUsersTemplate: data.Get("allowed_users_template").(bool), AllowedUsersTemplate: data.Get("allowed_users_template").(bool),
AllowedDomains: data.Get("allowed_domains").(string), AllowedDomains: data.Get("allowed_domains").(string),
AllowedDomainsTemplate: data.Get("allowed_domains_template").(bool),
DefaultUser: defaultUser, DefaultUser: defaultUser,
DefaultUserTemplate: data.Get("default_user_template").(bool), DefaultUserTemplate: data.Get("default_user_template").(bool),
AllowBareDomains: data.Get("allow_bare_domains").(bool), AllowBareDomains: data.Get("allow_bare_domains").(bool),
@ -750,6 +761,7 @@ func (b *backend) parseRole(role *sshRole) (map[string]interface{}, error) {
"allowed_users": role.AllowedUsers, "allowed_users": role.AllowedUsers,
"allowed_users_template": role.AllowedUsersTemplate, "allowed_users_template": role.AllowedUsersTemplate,
"allowed_domains": role.AllowedDomains, "allowed_domains": role.AllowedDomains,
"allowed_domains_template": role.AllowedDomainsTemplate,
"default_user": role.DefaultUser, "default_user": role.DefaultUser,
"default_user_template": role.DefaultUserTemplate, "default_user_template": role.DefaultUserTemplate,
"ttl": int64(ttl.Seconds()), "ttl": int64(ttl.Seconds()),

3
changelog/16056.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
secrets/ssh: Add allowed_domains_template to allow templating of allowed_domains.
```

View File

@ -146,7 +146,7 @@ This endpoint creates or updates a named role.
Use with caution. N.B.: if the type is `ca`, an empty list does not allow any user; Use with caution. N.B.: if the type is `ca`, an empty list does not allow any user;
instead you must use `*` to enable this behavior. instead you must use `*` to enable this behavior.
- `allowed_users_template` `(bool: false)` - If set, allowed_users can be specified - `allowed_users_template` `(bool: false)` - If set, `allowed_users` can be specified
using identity template policies. Non-templated users are also permitted. using identity template policies. Non-templated users are also permitted.
- `allowed_domains` `(string: "")`  The list of domains for which a client can - `allowed_domains` `(string: "")`  The list of domains for which a client can
@ -154,6 +154,10 @@ This endpoint creates or updates a named role.
credentials can be created for any domain. See also `allow_bare_domains` and credentials can be created for any domain. See also `allow_bare_domains` and
`allow_subdomains`. `allow_subdomains`.
- `allowed_domains_template` `(bool: false)` - If set, `allowed_domains` can be
specified using identity template policies. Non-templated domains are also
permitted.
- `key_option_specs` `(string: "")`  Specifies a comma separated option - `key_option_specs` `(string: "")`  Specifies a comma separated option
specification which will be prefixed to RSA keys in the remote host's specification which will be prefixed to RSA keys in the remote host's
authorized_keys file. N.B.: Vault does not check this string for validity. authorized_keys file. N.B.: Vault does not check this string for validity.