Skip to content

Commit 0ef6145

Browse files
authored
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`.
1 parent c8fe898 commit 0ef6145

File tree

4 files changed

+366
-78
lines changed

4 files changed

+366
-78
lines changed

builtin/logical/ssh/backend_test.go

Lines changed: 251 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import (
1919

2020
"golang.org/x/crypto/ssh"
2121

22+
"github.com/hashicorp/vault/builtin/credential/userpass"
2223
"github.com/hashicorp/vault/helper/testhelpers/docker"
2324
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
25+
vaulthttp "github.com/hashicorp/vault/http"
2426
"github.com/hashicorp/vault/vault"
2527
"github.com/mitchellh/mapstructure"
2628
)
@@ -122,6 +124,7 @@ SjOQL/GkH1nkRcDS9++aAAAAAmNhAQID
122124

123125
dockerImageTagSupportsRSA1 = "8.1_p1-r0-ls20"
124126
dockerImageTagSupportsNoRSA1 = "8.4_p1-r3-ls48"
127+
125128
)
126129

127130
func prepareTestContainer(t *testing.T, tag, caPublicKeyPEM string) (func(), string) {
@@ -158,7 +161,7 @@ func prepareTestContainer(t *testing.T, tag, caPublicKeyPEM string) (func(), str
158161

159162
// Install util-linux for non-busybox flock that supports timeout option
160163
err = testSSH("vaultssh", sshAddress, ssh.PublicKeys(signer), fmt.Sprintf(`
161-
set -e;
164+
set -e;
162165
sudo ln -s /config /home/vaultssh
163166
sudo apk add util-linux;
164167
echo "LogLevel DEBUG" | sudo tee -a /config/ssh_host_keys/sshd_config;
@@ -1318,6 +1321,252 @@ func TestBackend_DisallowUserProvidedKeyIDs(t *testing.T) {
13181321
logicaltest.Test(t, testCase)
13191322
}
13201323

1324+
func TestBackend_DefExtTemplatingEnabled(t *testing.T) {
1325+
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
1326+
defer cluster.Cleanup()
1327+
client := cluster.Cores[0].Client
1328+
1329+
// Get auth accessor for identity template.
1330+
auths, err := client.Sys().ListAuth()
1331+
if err != nil {
1332+
t.Fatal(err)
1333+
}
1334+
userpassAccessor := auths["userpass/"].Accessor
1335+
1336+
// Write SSH role.
1337+
_, err = client.Logical().Write("ssh/roles/test", map[string]interface{}{
1338+
"key_type": "ca",
1339+
"allowed_extensions": "login@zipzap.com",
1340+
"allow_user_certificates": true,
1341+
"allowed_users": "tuber",
1342+
"default_user": "tuber",
1343+
"default_extensions_template": true,
1344+
"default_extensions": map[string]interface{}{
1345+
"login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}",
1346+
},
1347+
})
1348+
if err != nil {
1349+
t.Fatal(err)
1350+
}
1351+
1352+
sshKeyID := "vault-userpass-"+testUserName+"-9bd0f01b7dfc50a13aa5e5cd11aea19276968755c8f1f9c98965d04147f30ed0"
1353+
1354+
// Issue SSH certificate with default extensions templating enabled, and no user-provided extensions
1355+
client.SetToken(userpassToken)
1356+
resp, err := client.Logical().Write("ssh/sign/test", map[string]interface{}{
1357+
"public_key": publicKey4096,
1358+
})
1359+
if err != nil {
1360+
t.Fatal(err)
1361+
}
1362+
signedKey := resp.Data["signed_key"].(string)
1363+
key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
1364+
1365+
parsedKey, err := ssh.ParsePublicKey(key)
1366+
if err != nil {
1367+
t.Fatal(err)
1368+
}
1369+
1370+
defaultExtensionPermissions := map[string]string{
1371+
"login@foobar.com": testUserName,
1372+
}
1373+
1374+
err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, defaultExtensionPermissions, 16*time.Hour)
1375+
if err != nil {
1376+
t.Fatal(err)
1377+
}
1378+
1379+
// Issue SSH certificate with default extensions templating enabled, and user-provided extensions
1380+
// The certificate should only have the user-provided extensions, and no templated extensions
1381+
userProvidedExtensionPermissions := map[string]string{
1382+
"login@zipzap.com": "some_other_user_name",
1383+
}
1384+
resp, err = client.Logical().Write("ssh/sign/test", map[string]interface{}{
1385+
"public_key": publicKey4096,
1386+
"extensions": userProvidedExtensionPermissions,
1387+
})
1388+
if err != nil {
1389+
t.Fatal(err)
1390+
}
1391+
signedKey = resp.Data["signed_key"].(string)
1392+
key, _ = base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
1393+
1394+
parsedKey, err = ssh.ParsePublicKey(key)
1395+
if err != nil {
1396+
t.Fatal(err)
1397+
}
1398+
1399+
err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, userProvidedExtensionPermissions, 16*time.Hour)
1400+
if err != nil {
1401+
t.Fatal(err)
1402+
}
1403+
1404+
// Issue SSH certificate with default extensions templating enabled, and invalid user-provided extensions - it should fail
1405+
invalidUserProvidedExtensionPermissions := map[string]string{
1406+
"login@foobar.com": "{{identity.entity.metadata}}",
1407+
}
1408+
resp, err = client.Logical().Write("ssh/sign/test", map[string]interface{}{
1409+
"public_key": publicKey4096,
1410+
"extensions": invalidUserProvidedExtensionPermissions,
1411+
})
1412+
if err == nil {
1413+
t.Fatal("expected an error while attempting to sign a key with invalid permissions")
1414+
}
1415+
}
1416+
1417+
func TestBackend_DefExtTemplatingDisabled(t *testing.T) {
1418+
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
1419+
defer cluster.Cleanup()
1420+
client := cluster.Cores[0].Client
1421+
1422+
// Get auth accessor for identity template.
1423+
auths, err := client.Sys().ListAuth()
1424+
if err != nil {
1425+
t.Fatal(err)
1426+
}
1427+
userpassAccessor := auths["userpass/"].Accessor
1428+
1429+
// Write SSH role to test with any extension. We also provide a templated default extension,
1430+
// to verify that it's not actually being evaluated
1431+
_, err = client.Logical().Write("ssh/roles/test_allow_all_extensions", map[string]interface{}{
1432+
"key_type": "ca",
1433+
"allow_user_certificates": true,
1434+
"allowed_users": "tuber",
1435+
"default_user": "tuber",
1436+
"default_extensions_template": false,
1437+
"default_extensions": map[string]interface{}{
1438+
"login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}",
1439+
},
1440+
})
1441+
if err != nil {
1442+
t.Fatal(err)
1443+
}
1444+
1445+
sshKeyID := "vault-userpass-"+testUserName+"-9bd0f01b7dfc50a13aa5e5cd11aea19276968755c8f1f9c98965d04147f30ed0"
1446+
1447+
// Issue SSH certificate with default extensions templating disabled, and no user-provided extensions
1448+
client.SetToken(userpassToken)
1449+
defaultExtensionPermissions := map[string]string{
1450+
"login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}",
1451+
"login@zipzap.com": "some_other_user_name",
1452+
}
1453+
resp, err := client.Logical().Write("ssh/sign/test_allow_all_extensions", map[string]interface{}{
1454+
"public_key": publicKey4096,
1455+
"extensions": defaultExtensionPermissions,
1456+
})
1457+
if err != nil {
1458+
t.Fatal(err)
1459+
}
1460+
signedKey := resp.Data["signed_key"].(string)
1461+
key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
1462+
1463+
parsedKey, err := ssh.ParsePublicKey(key)
1464+
if err != nil {
1465+
t.Fatal(err)
1466+
}
1467+
1468+
err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, defaultExtensionPermissions, 16*time.Hour)
1469+
if err != nil {
1470+
t.Fatal(err)
1471+
}
1472+
1473+
// Issue SSH certificate with default extensions templating disabled, and user-provided extensions
1474+
client.SetToken(userpassToken)
1475+
userProvidedAnyExtensionPermissions := map[string]string{
1476+
"login@foobar.com": "not_userpassname",
1477+
"login@zipzap.com": "some_other_user_name",
1478+
}
1479+
resp, err = client.Logical().Write("ssh/sign/test_allow_all_extensions", map[string]interface{}{
1480+
"public_key": publicKey4096,
1481+
"extensions": userProvidedAnyExtensionPermissions,
1482+
})
1483+
if err != nil {
1484+
t.Fatal(err)
1485+
}
1486+
signedKey = resp.Data["signed_key"].(string)
1487+
key, _ = base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
1488+
1489+
parsedKey, err = ssh.ParsePublicKey(key)
1490+
if err != nil {
1491+
t.Fatal(err)
1492+
}
1493+
1494+
err = validateSSHCertificate(parsedKey.(*ssh.Certificate), sshKeyID, ssh.UserCert, []string{"tuber"}, map[string]string{}, userProvidedAnyExtensionPermissions, 16*time.Hour)
1495+
if err != nil {
1496+
t.Fatal(err)
1497+
}
1498+
}
1499+
1500+
func getSshCaTestCluster(t *testing.T, userIdentity string) (*vault.TestCluster, string) {
1501+
coreConfig := &vault.CoreConfig{
1502+
CredentialBackends: map[string]logical.Factory{
1503+
"userpass": userpass.Factory,
1504+
},
1505+
LogicalBackends: map[string]logical.Factory{
1506+
"ssh": Factory,
1507+
},
1508+
}
1509+
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
1510+
HandlerFunc: vaulthttp.Handler,
1511+
})
1512+
cluster.Start()
1513+
client := cluster.Cores[0].Client
1514+
1515+
// Write test policy for userpass auth method.
1516+
err := client.Sys().PutPolicy("test", `
1517+
path "ssh/*" {
1518+
capabilities = ["update"]
1519+
}`)
1520+
if err != nil {
1521+
t.Fatal(err)
1522+
}
1523+
1524+
// Enable userpass auth method.
1525+
if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil {
1526+
t.Fatal(err)
1527+
}
1528+
1529+
// Configure test role for userpass.
1530+
if _, err := client.Logical().Write("auth/userpass/users/"+userIdentity, map[string]interface{}{
1531+
"password": "test",
1532+
"policies": "test",
1533+
}); err != nil {
1534+
t.Fatal(err)
1535+
}
1536+
1537+
// Login userpass for test role and keep client token.
1538+
secret, err := client.Logical().Write("auth/userpass/login/"+userIdentity, map[string]interface{}{
1539+
"password": "test",
1540+
})
1541+
if err != nil || secret == nil {
1542+
t.Fatal(err)
1543+
}
1544+
userpassToken := secret.Auth.ClientToken
1545+
1546+
// Mount SSH.
1547+
err = client.Sys().Mount("ssh", &api.MountInput{
1548+
Type: "ssh",
1549+
Config: api.MountConfigInput{
1550+
DefaultLeaseTTL: "16h",
1551+
MaxLeaseTTL: "60h",
1552+
},
1553+
})
1554+
if err != nil {
1555+
t.Fatal(err)
1556+
}
1557+
1558+
// Configure SSH CA.
1559+
_, err = client.Logical().Write("ssh/config/ca", map[string]interface{}{
1560+
"public_key": testCAPublicKey,
1561+
"private_key": testCAPrivateKey,
1562+
})
1563+
if err != nil {
1564+
t.Fatal(err)
1565+
}
1566+
1567+
return cluster, userpassToken
1568+
}
1569+
13211570
func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep {
13221571
return logicaltest.TestStep{
13231572
Operation: logical.UpdateOperation,
@@ -1391,7 +1640,7 @@ func validateSSHCertificate(cert *ssh.Certificate, keyID string, certType int, v
13911640

13921641
actualTTL := time.Unix(int64(cert.ValidBefore), 0).Add(-30 * time.Second).Sub(time.Unix(int64(cert.ValidAfter), 0))
13931642
if actualTTL != ttl {
1394-
return fmt.Errorf("incorrect ttl: expected: %v, actualL %v", ttl, actualTTL)
1643+
return fmt.Errorf("incorrect ttl: expected: %v, actual %v", ttl, actualTTL)
13951644
}
13961645

13971646
if !reflect.DeepEqual(cert.ValidPrincipals, validPrincipals) {

0 commit comments

Comments
 (0)