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:
parent
dc4965f27d
commit
21a10e09b6
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
secrets/ssh: Add allowed_domains_template to allow templating of allowed_domains.
|
||||||
|
```
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue