Add support for templated values in SSH CA DefaultExtensions. (#11495)
* Add support for templated values in SSH CA DefaultExtensions. * Reworking the logic per feedback, adding basic test. * Adding test, so we cover both default extension templating & ignoring default when user-provided extensions are present. * Fixed up an unintentional extension handling defect, added test to cover the case. * Refactor Default Extension tests into `enabled` and `disabled`.
This commit is contained in:
parent
e56982f782
commit
491f71faf0
|
@ -19,8 +19,10 @@ import (
|
|||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/hashicorp/vault/builtin/credential/userpass"
|
||||
"github.com/hashicorp/vault/helper/testhelpers/docker"
|
||||
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
|
||||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
@ -122,6 +124,7 @@ SjOQL/GkH1nkRcDS9++aAAAAAmNhAQID
|
|||
|
||||
dockerImageTagSupportsRSA1 = "8.1_p1-r0-ls20"
|
||||
dockerImageTagSupportsNoRSA1 = "8.4_p1-r3-ls48"
|
||||
|
||||
)
|
||||
|
||||
func prepareTestContainer(t *testing.T, tag, caPublicKeyPEM string) (func(), string) {
|
||||
|
@ -1318,6 +1321,252 @@ func TestBackend_DisallowUserProvidedKeyIDs(t *testing.T) {
|
|||
logicaltest.Test(t, testCase)
|
||||
}
|
||||
|
||||
func TestBackend_DefExtTemplatingEnabled(t *testing.T) {
|
||||
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Get auth accessor for identity template.
|
||||
auths, err := client.Sys().ListAuth()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
userpassAccessor := auths["userpass/"].Accessor
|
||||
|
||||
// Write SSH role.
|
||||
_, err = client.Logical().Write("ssh/roles/test", map[string]interface{}{
|
||||
"key_type": "ca",
|
||||
"allowed_extensions": "login@zipzap.com",
|
||||
"allow_user_certificates": true,
|
||||
"allowed_users": "tuber",
|
||||
"default_user": "tuber",
|
||||
"default_extensions_template": true,
|
||||
"default_extensions": map[string]interface{}{
|
||||
"login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sshKeyID := "vault-userpass-"+testUserName+"-9bd0f01b7dfc50a13aa5e5cd11aea19276968755c8f1f9c98965d04147f30ed0"
|
||||
|
||||
// Issue SSH certificate with default extensions templating enabled, and no user-provided extensions
|
||||
client.SetToken(userpassToken)
|
||||
resp, err := client.Logical().Write("ssh/sign/test", map[string]interface{}{
|
||||
"public_key": publicKey4096,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signedKey := resp.Data["signed_key"].(string)
|
||||
key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
|
||||
|
||||
parsedKey, err := ssh.ParsePublicKey(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defaultExtensionPermissions := map[string]string{
|
||||
"login@foobar.com": testUserName,
|
||||
}
|
||||
|
||||
err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, defaultExtensionPermissions, 16*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Issue SSH certificate with default extensions templating enabled, and user-provided extensions
|
||||
// The certificate should only have the user-provided extensions, and no templated extensions
|
||||
userProvidedExtensionPermissions := map[string]string{
|
||||
"login@zipzap.com": "some_other_user_name",
|
||||
}
|
||||
resp, err = client.Logical().Write("ssh/sign/test", map[string]interface{}{
|
||||
"public_key": publicKey4096,
|
||||
"extensions": userProvidedExtensionPermissions,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signedKey = resp.Data["signed_key"].(string)
|
||||
key, _ = base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
|
||||
|
||||
parsedKey, err = ssh.ParsePublicKey(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, userProvidedExtensionPermissions, 16*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Issue SSH certificate with default extensions templating enabled, and invalid user-provided extensions - it should fail
|
||||
invalidUserProvidedExtensionPermissions := map[string]string{
|
||||
"login@foobar.com": "{{identity.entity.metadata}}",
|
||||
}
|
||||
resp, err = client.Logical().Write("ssh/sign/test", map[string]interface{}{
|
||||
"public_key": publicKey4096,
|
||||
"extensions": invalidUserProvidedExtensionPermissions,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error while attempting to sign a key with invalid permissions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackend_DefExtTemplatingDisabled(t *testing.T) {
|
||||
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Get auth accessor for identity template.
|
||||
auths, err := client.Sys().ListAuth()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
userpassAccessor := auths["userpass/"].Accessor
|
||||
|
||||
// Write SSH role to test with any extension. We also provide a templated default extension,
|
||||
// to verify that it's not actually being evaluated
|
||||
_, err = client.Logical().Write("ssh/roles/test_allow_all_extensions", map[string]interface{}{
|
||||
"key_type": "ca",
|
||||
"allow_user_certificates": true,
|
||||
"allowed_users": "tuber",
|
||||
"default_user": "tuber",
|
||||
"default_extensions_template": false,
|
||||
"default_extensions": map[string]interface{}{
|
||||
"login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sshKeyID := "vault-userpass-"+testUserName+"-9bd0f01b7dfc50a13aa5e5cd11aea19276968755c8f1f9c98965d04147f30ed0"
|
||||
|
||||
// Issue SSH certificate with default extensions templating disabled, and no user-provided extensions
|
||||
client.SetToken(userpassToken)
|
||||
defaultExtensionPermissions := map[string]string{
|
||||
"login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}",
|
||||
"login@zipzap.com": "some_other_user_name",
|
||||
}
|
||||
resp, err := client.Logical().Write("ssh/sign/test_allow_all_extensions", map[string]interface{}{
|
||||
"public_key": publicKey4096,
|
||||
"extensions": defaultExtensionPermissions,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signedKey := resp.Data["signed_key"].(string)
|
||||
key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
|
||||
|
||||
parsedKey, err := ssh.ParsePublicKey(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, defaultExtensionPermissions, 16*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Issue SSH certificate with default extensions templating disabled, and user-provided extensions
|
||||
client.SetToken(userpassToken)
|
||||
userProvidedAnyExtensionPermissions := map[string]string{
|
||||
"login@foobar.com": "not_userpassname",
|
||||
"login@zipzap.com": "some_other_user_name",
|
||||
}
|
||||
resp, err = client.Logical().Write("ssh/sign/test_allow_all_extensions", map[string]interface{}{
|
||||
"public_key": publicKey4096,
|
||||
"extensions": userProvidedAnyExtensionPermissions,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signedKey = resp.Data["signed_key"].(string)
|
||||
key, _ = base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
|
||||
|
||||
parsedKey, err = ssh.ParsePublicKey(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, userProvidedAnyExtensionPermissions, 16*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getSshCaTestCluster(t *testing.T, userIdentity string) (*vault.TestCluster, string) {
|
||||
coreConfig := &vault.CoreConfig{
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"userpass": userpass.Factory,
|
||||
},
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
"ssh": Factory,
|
||||
},
|
||||
}
|
||||
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Write test policy for userpass auth method.
|
||||
err := client.Sys().PutPolicy("test", `
|
||||
path "ssh/*" {
|
||||
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/"+userIdentity, 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/"+userIdentity, map[string]interface{}{
|
||||
"password": "test",
|
||||
})
|
||||
if err != nil || secret == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
userpassToken := secret.Auth.ClientToken
|
||||
|
||||
// Mount SSH.
|
||||
err = client.Sys().Mount("ssh", &api.MountInput{
|
||||
Type: "ssh",
|
||||
Config: api.MountConfigInput{
|
||||
DefaultLeaseTTL: "16h",
|
||||
MaxLeaseTTL: "60h",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Configure SSH CA.
|
||||
_, err = client.Logical().Write("ssh/config/ca", map[string]interface{}{
|
||||
"public_key": testCAPublicKey,
|
||||
"private_key": testCAPrivateKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return cluster, userpassToken
|
||||
}
|
||||
|
||||
func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.UpdateOperation,
|
||||
|
@ -1391,7 +1640,7 @@ func validateSSHCertificate(cert *ssh.Certificate, keyID string, certType int, v
|
|||
|
||||
actualTTL := time.Unix(int64(cert.ValidBefore), 0).Add(-30 * time.Second).Sub(time.Unix(int64(cert.ValidAfter), 0))
|
||||
if actualTTL != ttl {
|
||||
return fmt.Errorf("incorrect ttl: expected: %v, actualL %v", ttl, actualTTL)
|
||||
return fmt.Errorf("incorrect ttl: expected: %v, actual %v", ttl, actualTTL)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cert.ValidPrincipals, validPrincipals) {
|
||||
|
|
|
@ -43,6 +43,7 @@ type sshRole struct {
|
|||
TTL string `mapstructure:"ttl" json:"ttl"`
|
||||
DefaultCriticalOptions map[string]string `mapstructure:"default_critical_options" json:"default_critical_options"`
|
||||
DefaultExtensions map[string]string `mapstructure:"default_extensions" json:"default_extensions"`
|
||||
DefaultExtensionsTemplate bool `mapstructure:"default_extensions_template" json:"default_extensions_template"`
|
||||
AllowedCriticalOptions string `mapstructure:"allowed_critical_options" json:"allowed_critical_options"`
|
||||
AllowedExtensions string `mapstructure:"allowed_extensions" json:"allowed_extensions"`
|
||||
AllowUserCertificates bool `mapstructure:"allow_user_certificates" json:"allow_user_certificates"`
|
||||
|
@ -267,6 +268,15 @@ func pathRoles(b *backend) *framework.Path {
|
|||
"allowed_extensions". Defaults to none.
|
||||
`,
|
||||
},
|
||||
"default_extensions_template": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `
|
||||
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
|
||||
If set, Default extension values can be specified using identity template policies.
|
||||
Non-templated extension values are also permitted.
|
||||
`,
|
||||
Default: false,
|
||||
},
|
||||
"allow_user_certificates": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `
|
||||
|
@ -525,6 +535,7 @@ func (b *backend) createCARole(allowedUsers, defaultUser, signer string, data *f
|
|||
AllowBareDomains: data.Get("allow_bare_domains").(bool),
|
||||
AllowSubdomains: data.Get("allow_subdomains").(bool),
|
||||
AllowUserKeyIDs: data.Get("allow_user_key_ids").(bool),
|
||||
DefaultExtensionsTemplate: data.Get("default_extensions_template").(bool),
|
||||
KeyIDFormat: data.Get("key_id_format").(string),
|
||||
KeyType: KeyTypeCA,
|
||||
AlgorithmSigner: signer,
|
||||
|
@ -618,6 +629,7 @@ func (b *backend) parseRole(role *sshRole) (map[string]interface{}, error) {
|
|||
"key_bits": role.KeyBits,
|
||||
"default_critical_options": role.DefaultCriticalOptions,
|
||||
"default_extensions": role.DefaultExtensions,
|
||||
"default_extensions_template": role.DefaultExtensionsTemplate,
|
||||
"allowed_user_key_lengths": role.AllowedUserKeyLengths,
|
||||
"algorithm_signer": role.AlgorithmSigner,
|
||||
}
|
||||
|
|
|
@ -155,7 +155,7 @@ func (b *backend) pathSignCertificate(ctx context.Context, req *logical.Request,
|
|||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
extensions, err := b.calculateExtensions(data, role)
|
||||
extensions, err := b.calculateExtensions(data, req, role)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
|
@ -356,21 +356,19 @@ func (b *backend) calculateCriticalOptions(data *framework.FieldData, role *sshR
|
|||
return criticalOptions, nil
|
||||
}
|
||||
|
||||
func (b *backend) calculateExtensions(data *framework.FieldData, role *sshRole) (map[string]string, error) {
|
||||
func (b *backend) calculateExtensions(data *framework.FieldData, req *logical.Request, role *sshRole) (map[string]string, error) {
|
||||
unparsedExtensions := data.Get("extensions").(map[string]interface{})
|
||||
if len(unparsedExtensions) == 0 {
|
||||
return role.DefaultExtensions, nil
|
||||
}
|
||||
extensions := make(map[string]string)
|
||||
|
||||
if len(unparsedExtensions) > 0 {
|
||||
extensions := convertMapToStringValue(unparsedExtensions)
|
||||
|
||||
if role.AllowedExtensions != "" {
|
||||
notAllowed := []string{}
|
||||
allowedExtensions := strings.Split(role.AllowedExtensions, ",")
|
||||
|
||||
for extension := range extensions {
|
||||
if !strutil.StrListContains(allowedExtensions, extension) {
|
||||
notAllowed = append(notAllowed, extension)
|
||||
for extensionKey, _ := range extensions {
|
||||
if !strutil.StrListContains(allowedExtensions, extensionKey) {
|
||||
notAllowed = append(notAllowed, extensionKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -378,6 +376,32 @@ func (b *backend) calculateExtensions(data *framework.FieldData, role *sshRole)
|
|||
return nil, fmt.Errorf("extensions %v are not on allowed list", notAllowed)
|
||||
}
|
||||
}
|
||||
return extensions, nil
|
||||
}
|
||||
|
||||
if role.DefaultExtensionsTemplate {
|
||||
for extensionKey, extensionValue := range role.DefaultExtensions {
|
||||
// Look for templating markers {{ .* }}
|
||||
matched, _ := regexp.MatchString(`^{{.+?}}$`, extensionValue)
|
||||
if matched {
|
||||
if req.EntityID != "" {
|
||||
// Retrieve extension value based on template + entityID from request.
|
||||
templateExtensionValue, err := framework.PopulateIdentityTemplate(extensionValue, req.EntityID, b.System())
|
||||
if err == nil {
|
||||
// Template returned an extension value that we can use
|
||||
extensions[extensionKey] = templateExtensionValue
|
||||
} else {
|
||||
return nil, fmt.Errorf("template '%s' could not be rendered -> %s", extensionValue, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Static extension value or err template
|
||||
extensions[extensionKey] = extensionValue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
extensions = role.DefaultExtensions
|
||||
}
|
||||
|
||||
return extensions, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
ssh: add support for templated values in SSH CA DefaultExtensions
|
||||
```
|
Loading…
Reference in New Issue