Skip to content

Commit

Permalink
impr(ssh): fix bug with allowed_users_template and add allowed_domain…
Browse files Browse the repository at this point in the history
…s_template field in SSH role configuration, closes hashicorp#10943
  • Loading branch information
f4z3r committed Jun 18, 2022
1 parent 1e8004d commit fb10e2f
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 17 deletions.
60 changes: 48 additions & 12 deletions builtin/logical/ssh/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
testAllowedUsersTemplate(t,
"{{ identity.entity.metadata.ssh_username }}",
Expand Down Expand Up @@ -1846,8 +1871,9 @@ func getSshCaTestCluster(t *testing.T, userIdentity string) (*vault.TestCluster,
return cluster, userpassToken
}

func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
expectedValidPrincipal string, testEntityMetadata map[string]string) {
func testAllowedPrincipalsTemplate(t *testing.T, testAllowedDomainsTemplate string,
expectedValidPrincipal string, testEntityMetadata map[string]string,
roleConfigPayload map[string]interface{}, signingPayload map[string]interface{}) {
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
defer cluster.Cleanup()
client := cluster.Cores[0].Client
Expand All @@ -1867,22 +1893,14 @@ func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
t.Fatal(err)
}

_, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{
"key_type": testCaKeyType,
"allow_user_certificates": true,
"allowed_users": testAllowedUsersTemplate,
"allowed_users_template": true,
})
_, err = client.Logical().Write("ssh/roles/my-role", roleConfigPayload)
if err != nil {
t.Fatal(err)
}

// sign SSH key as userpass user
client.SetToken(userpassToken)
signResponse, err := client.Logical().Write("ssh/sign/my-role", map[string]interface{}{
"public_key": testCAPublicKey,
"valid_principals": expectedValidPrincipal,
})
signResponse, err := client.Logical().Write("ssh/sign/my-role", signingPayload)
if err != nil {
t.Fatal(err)
}
Expand All @@ -1903,6 +1921,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 {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Expand Down
12 changes: 12 additions & 0 deletions builtin/logical/ssh/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type sshRole struct {
AllowedUsers string `mapstructure:"allowed_users" json:"allowed_users"`
AllowedUsersTemplate bool `mapstructure:"allowed_users_template" json:"allowed_users_template"`
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"`
MaxTTL string `mapstructure:"max_ttl" json:"max_ttl"`
TTL string `mapstructure:"ttl" json:"ttl"`
Expand Down Expand Up @@ -213,6 +214,15 @@ func pathRoles(b *backend) *framework.Path {
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": {
Type: framework.TypeString,
Description: `
Expand Down Expand Up @@ -557,6 +567,7 @@ func (b *backend) createCARole(allowedUsers, defaultUser, signer string, data *f
AllowedUsers: allowedUsers,
AllowedUsersTemplate: data.Get("allowed_users_template").(bool),
AllowedDomains: data.Get("allowed_domains").(string),
AllowedDomainsTemplate: data.Get("allowed_domains_template").(bool),
DefaultUser: defaultUser,
AllowBareDomains: data.Get("allow_bare_domains").(bool),
AllowSubdomains: data.Get("allow_subdomains").(bool),
Expand Down Expand Up @@ -739,6 +750,7 @@ func (b *backend) parseRole(role *sshRole) (map[string]interface{}, error) {
"allowed_users": role.AllowedUsers,
"allowed_users_template": role.AllowedUsersTemplate,
"allowed_domains": role.AllowedDomains,
"allowed_domains_template": role.AllowedDomainsTemplate,
"default_user": role.DefaultUser,
"ttl": int64(ttl.Seconds()),
"max_ttl": int64(maxTTL.Seconds()),
Expand Down
8 changes: 4 additions & 4 deletions builtin/logical/ssh/path_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,12 @@ func (b *backend) pathSignCertificate(ctx context.Context, req *logical.Request,

var parsedPrincipals []string
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 {
return logical.ErrorResponse(err.Error()), nil
}
} else {
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, role.DefaultUser, role.AllowedUsers, strutil.StrListContains)
parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, role.DefaultUser, role.AllowedUsers, role.AllowedUsersTemplate, strutil.StrListContains)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
Expand Down Expand Up @@ -205,7 +205,7 @@ func (b *backend) pathSignCertificate(ctx context.Context, req *logical.Request,
return response, 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 := ""
validPrincipalsRaw, ok := data.GetOk("valid_principals")
if ok {
Expand All @@ -218,7 +218,7 @@ func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logic
// Build list of allowed Principals from template and static principalsAllowedByRole
var allowedPrincipals []string
for _, principal := range strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) {
if role.AllowedUsersTemplate {
if enableTemplating {
// Look for templating markers {{ .* }}
matched, _ := regexp.MatchString(`{{.+?}}`, principal)
if matched {
Expand Down
6 changes: 5 additions & 1 deletion website/content/api-docs/secret/ssh.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,18 @@ 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;
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.

- `allowed_domains` `(string: "")` – The list of domains for which a client can
request a host certificate. If this option is explicitly set to `"*"`, then
credentials can be created for any domain. See also `allow_bare_domains` and
`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
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.
Expand Down

0 comments on commit fb10e2f

Please sign in to comment.