package ssh import ( "bytes" "context" "encoding/base64" "errors" "fmt" "net" "reflect" "strings" "testing" "time" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/logical" "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" ) const ( testIP = "127.0.0.1" testUserName = "vaultssh" testMultiUserName = "vaultssh,otherssh" testAdminUser = "vaultssh" testCaKeyType = "ca" testOTPKeyType = "otp" testCIDRList = "127.0.0.1/32" testAtRoleName = "test@RoleName" testOTPRoleName = "testOTPRoleName" // testKeyName is the name of the entry that will be written to SSHMOUNTPOINT/ssh/keys testKeyName = "testKeyName" // testSharedPrivateKey is the value of the entry that will be written to SSHMOUNTPOINT/ssh/keys testSharedPrivateKey = ` -----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAvYvoRcWRxqOim5VZnuM6wHCbLUeiND0yaM1tvOl+Fsrz55DG A0OZp4RGAu1Fgr46E1mzxFz1+zY4UbcEExg+u21fpa8YH8sytSWW1FyuD8ICib0A /l8slmDMw4BkkGOtSlEqgscpkpv/TWZD1NxJWkPcULk8z6c7TOETn2/H9mL+v2RE mbE6NDEwJKfD3MvlpIqCP7idR+86rNBAODjGOGgyUbtFLT+K01XmDRALkV3V/nh+ GltyjL4c6RU4zG2iRyV5RHlJtkml+UzUMkzr4IQnkCC32CC/wmtoo/IsAprpcHVe nkBn3eFQ7uND70p5n6GhN/KOh2j519JFHJyokwIDAQABAoIBAHX7VOvBC3kCN9/x +aPdup84OE7Z7MvpX6w+WlUhXVugnmsAAVDczhKoUc/WktLLx2huCGhsmKvyVuH+ MioUiE+vx75gm3qGx5xbtmOfALVMRLopjCnJYf6EaFA0ZeQ+NwowNW7Lu0PHmAU8 Z3JiX8IwxTz14DU82buDyewO7v+cEr97AnERe3PUcSTDoUXNaoNxjNpEJkKREY6h 4hAY676RT/GsRcQ8tqe/rnCqPHNd7JGqL+207FK4tJw7daoBjQyijWuB7K5chSal oPInylM6b13ASXuOAOT/2uSUBWmFVCZPDCmnZxy2SdnJGbsJAMl7Ma3MUlaGvVI+ Tfh1aQkCgYEA4JlNOabTb3z42wz6mz+Nz3JRwbawD+PJXOk5JsSnV7DtPtfgkK9y 6FTQdhnozGWShAvJvc+C4QAihs9AlHXoaBY5bEU7R/8UK/pSqwzam+MmxmhVDV7G IMQPV0FteoXTaJSikhZ88mETTegI2mik+zleBpVxvfdhE5TR+lq8Br0CgYEA2AwJ CUD5CYUSj09PluR0HHqamWOrJkKPFPwa+5eiTTCzfBBxImYZh7nXnWuoviXC0sg2 AuvCW+uZ48ygv/D8gcz3j1JfbErKZJuV+TotK9rRtNIF5Ub7qysP7UjyI7zCssVM kuDd9LfRXaB/qGAHNkcDA8NxmHW3gpln4CFdSY8CgYANs4xwfercHEWaJ1qKagAe rZyrMpffAEhicJ/Z65lB0jtG4CiE6w8ZeUMWUVJQVcnwYD+4YpZbX4S7sJ0B8Ydy AhkSr86D/92dKTIt2STk6aCN7gNyQ1vW198PtaAWH1/cO2UHgHOy3ZUt5X/Uwxl9 cex4flln+1Viumts2GgsCQKBgCJH7psgSyPekK5auFdKEr5+Gc/jB8I/Z3K9+g4X 5nH3G1PBTCJYLw7hRzw8W/8oALzvddqKzEFHphiGXK94Lqjt/A4q1OdbCrhiE68D My21P/dAKB1UYRSs9Y8CNyHCjuZM9jSMJ8vv6vG/SOJPsnVDWVAckAbQDvlTHC9t O98zAoGAcbW6uFDkrv0XMCpB9Su3KaNXOR0wzag+WIFQRXCcoTvxVi9iYfUReQPi oOyBJU/HMVvBfv4g+OVFLVgSwwm6owwsouZ0+D/LasbuHqYyqYqdyPJQYzWA2Y+F +B6f4RoPdSXj24JHPg/ioRxjaj094UXJxua2yfkcecGNEuBQHSs= -----END RSA PRIVATE KEY----- ` // Public half of `testCAPrivateKey`, identical to how it would be fed in from a file testCAPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDArgK0ilRRfk8E7HIsjz5l3BuxmwpDd8DHRCVfOhbZ4gOSVxjEOOqBwWGjygdboBIZwFXmwDlU6sWX0hBJAgpQz0Cjvbjxtq/NjkvATrYPgnrXUhTaEn2eQO0PsqRNSFH46SK/oJfTp0q8/WgojxWJ2L7FUV8PO8uIk49DzqAqPV7WXU63vFsjx+3WQOX/ILeQvHCvaqs3dWjjzEoDudRWCOdUqcHEOshV9azIzPrXlQVzRV3QAKl6u7pC+/Secorpwt6IHpMKoVPGiR0tMMuNOVH8zrAKzIxPGfy2WmNDpJopbXMTvSOGAqNcp49O4SKOQl9Fzfq2HEevJamKLrMB dummy@example.com ` publicKey2 = `AAAAB3NzaC1yc2EAAAADAQABAAABAQDArgK0ilRRfk8E7HIsjz5l3BuxmwpDd8DHRCVfOhbZ4gOSVxjEOOqBwWGjygdboBIZwFXmwDlU6sWX0hBJAgpQz0Cjvbjxtq/NjkvATrYPgnrXUhTaEn2eQO0PsqRNSFH46SK/oJfTp0q8/WgojxWJ2L7FUV8PO8uIk49DzqAqPV7WXU63vFsjx+3WQOX/ILeQvHCvaqs3dWjjzEoDudRWCOdUqcHEOshV9azIzPrXlQVzRV3QAKl6u7pC+/Secorpwt6IHpMKoVPGiR0tMMuNOVH8zrAKzIxPGfy2WmNDpJopbXMTvSOGAqNcp49O4SKOQl9Fzfq2HEevJamKLrMB ` publicKey3072 = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDlsMr3K1d0nzE1TjUULPRuVjEGETmOqHtWq4gVPq3HiuNVHE/e/BJnkXc40BoClQ2Z5ZZPJZ6izF9PnlzNDjpq8DrILUrn/6KrzCHvRwnkYMAXbfM/Br09z5QGptbOe1EMLeVe0b/udmUicbYAGPxMruZk+ljyr4vXkO+gOAIrxeSIQSdMVLU4g0pCPQuDCOx5IQpDYSlOB3091frpN8npfMueKPflNYzxnqqYgAVeDKAIqMCGOMOHUeIZJ7A7HuynEAVOsOkJwC9nesy9D6ppdWNduGl42IkzlwVdDMZtUAEznMUT/dnHNG1Krx9SuNZ/S9fGjxGVsT+jzUmizrWB9/6XIEHDxPBzcqlWFuwYTGz1OL8bfZ+HldOGPcnqZn9hKntWwjUc3whcvWt+NCmXpHSVLSxf+WN8pdmfEsCqn8mpvo2MXa+iJrtAVPX4i0u8AQUuqC3NuXHv4Cn0LNwtziBT544UjgbWkAZqzFZJREYA09OHscc3akEIrTnPehk= demo@example.com` publicKey4096 = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC54Oj4YCFDYxYv69Q9KfU6rWYtUB1eByQdUW0nXFi/vr98QUIV77sEeUVhaQzZcuCojAi/GrloW7ta0Z2DaEv5jOQMAnGpXBcqLJsz3KdrHbpvl93MPNdmNaGPU0GnUEsjBVuDVn9HdIUa8CNrxShvPu7/VqoaRHKLqphGgzFb37vi4qvnQ+5VYAO/TzyVYMD6qJX6I/9Pw8d74jCfEdOh2yGKkP7rXWOghreyIl8H2zTJKg9KoZuPq9F5M8nNt7Oi3rf+DwQiYvamzIqlDP4s5oFVTZW0E9lwWvYDpyiJnUrkQqksebBK/rcyfiFG3onb4qLo2WVWXeK3si8IhGik/TEzprScyAWIf9RviT8O+l5hTA2/c+ctn3MVCLRNfez2lKpdxCoprv1MbIcySGWblTJEcY6RA+aauVJpu7FMtRxHHtZKtMpep8cLu8GKbiP6Ifq2JXBtXtNxDeIgo2MkNoMh/NHAsACJniE/dqV/+u9HvhvgrTbJ69ell0nE4ivzA7O4kZgbR/4MHlLgLFvaqC8RrWRLY6BdFagPIMxghWha7Qw16zqoIjRnolvRzUWvSXanJVg8Z6ua1VxwgirNaAH1ivmJhUh2+4lNxCX6jmZyR3zjJsWY03gjJTairvI762opjjalF8fH6Xrs15mB14JiAlNbk6+5REQcvXlGqw== dummy@example.com` testCAPrivateKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAwK4CtIpUUX5PBOxyLI8+ZdwbsZsKQ3fAx0QlXzoW2eIDklcY xDjqgcFho8oHW6ASGcBV5sA5VOrFl9IQSQIKUM9Ao7248bavzY5LwE62D4J611IU 2hJ9nkDtD7KkTUhR+Okiv6CX06dKvP1oKI8Vidi+xVFfDzvLiJOPQ86gKj1e1l1O t7xbI8ft1kDl/yC3kLxwr2qrN3Vo48xKA7nUVgjnVKnBxDrIVfWsyMz615UFc0Vd 0ACperu6Qvv0nnKK6cLeiB6TCqFTxokdLTDLjTlR/M6wCsyMTxn8tlpjQ6SaKW1z E70jhgKjXKePTuEijkJfRc36thxHryWpii6zAQIDAQABAoIBAA/DrPD8iF2KigiL F+RRa/eFhLaJStOuTpV/G9eotwnolgY5Hguf5H/tRIHUG7oBZLm6pMyWWZp7AuOj CjYO9q0Z5939vc349nVI+SWoyviF4msPiik1bhWulja8lPjFu/8zg+ZNy15Dx7ei vAzleAupMiKOv8pNSB/KguQ3WZ9a9bcQcoFQ2Foru6mXpLJ03kghVRlkqvQ7t5cA n11d2Hiipq9mleESr0c+MUPKLBX/neaWfGA4xgJTjIYjZi6avmYc/Ox3sQ9aLq2J tH0D4HVUZvaU28hn+jhbs64rRFbu++qQMe3vNvi/Q/iqcYU4b6tgDNzm/JFRTS/W njiz4mkCgYEA44CnQVmonN6qQ0AgNNlBY5+RX3wwBJZ1AaxpzwDRylAt2vlVUA0n YY4RW4J4+RMRKwHwjxK5RRmHjsIJx+nrpqihW3fte3ev5F2A9Wha4dzzEHxBY6IL 362T/x2f+vYk6tV+uTZSUPHsuELH26mitbBVFNB/00nbMNdEc2bO5FMCgYEA2NCw ubt+g2bRkkT/Qf8gIM8ZDpZbARt6onqxVcWkQFT16ZjbsBWUrH1Xi7alv9+lwYLJ ckY/XDX4KeU19HabeAbpyy6G9Q2uBSWZlJbjl7QNhdLeuzV82U1/r8fy6Uu3gQnU WSFx2GesRpSmZpqNKMs5ksqteZ9Yjg1EIgXdINsCgYBIn9REt1NtKGOf7kOZu1T1 cYXdvm4xuLoHW7u3OiK+e9P3mCqU0G4m5UxDMyZdFKohWZAqjCaamWi9uNGYgOMa I7DG20TzaiS7OOIm9TY17eul8pSJMrypnealxRZB7fug/6Bhjaa/cktIEwFr7P4l E/JFH73+fBA9yipu0H3xQwKBgHmiwrLAZF6VrVcxDD9bQQwHA5iyc4Wwg+Fpkdl7 0wUgZQHTdtRXlxwaCaZhJqX5c4WXuSo6DMvPn1TpuZZXgCsbPch2ZtJOBWXvzTSW XkK6iaedQMWoYU2L8+mK9FU73EwxVodWgwcUSosiVCRV6oGLWdZnjGEiK00uVh38 Si1nAoGBAL47wWinv1cDTnh5mm0mybz3oI2a6V9aIYCloQ/EFcvtahyR/gyB8qNF lObH9Faf0WGdnACZvTz22U9gWhw79S0SpDV31tC5Kl8dXHFiZ09vYUKkYmSd/kms SeKWrUkryx46LVf6NMhkyYmRqCEjBwfOozzezi5WbiJy6nn54GQt -----END RSA PRIVATE KEY----- ` testCAPublicKeyEd25519 = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO1S6g5Bib7vT8eoFnvTl3dZSjOQL/GkH1nkRcDS9++a ca ` testCAPrivateKeyEd25519 = `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACDtUuoOQYm+70/HqBZ705d3WUozkC/xpB9Z5EXA0vfvmgAAAIhfRuszX0br MwAAAAtzc2gtZWQyNTUxOQAAACDtUuoOQYm+70/HqBZ705d3WUozkC/xpB9Z5EXA0vfvmg AAAEBQYa029SP/7AGPFQLmzwOc9eCoOZuwCq3iIf2C6fj9j+1S6g5Bib7vT8eoFnvTl3dZ SjOQL/GkH1nkRcDS9++aAAAAAmNhAQID -----END OPENSSH PRIVATE KEY----- ` publicKeyECDSA256 = `ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJsfOouYIjJNI23QJqaDsFTGukm21fRAMeGvKZDB59i5jnX1EubMH1AEjjzz4fgySUlyWKo+TS31rxU8kX3DDM4= demo@example.com` publicKeyECDSA521 = `ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAEg73ORD4J3FV2CrL01gLSKREO2EHrZPlJCOeDL5OKD3M1GCHv3q8O452RW49Aw+8zFFFU5u6d1Ys3Qsj05zdaQwQDt/D3ceWLGVkWiKyLPQStfn0GGOZh3YFKEw5XmeW9jh6xudEHlKs4Pfv2FrroaUKZvM2SlxR/feOK0tCQyq3MN/g== demo@example.com` // testPublicKeyInstall is the public key that is installed in the // admin account's authorized_keys testPublicKeyInstall = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9i+hFxZHGo6KblVme4zrAcJstR6I0PTJozW286X4WyvPnkMYDQ5mnhEYC7UWCvjoTWbPEXPX7NjhRtwQTGD67bV+lrxgfyzK1JZbUXK4PwgKJvQD+XyyWYMzDgGSQY61KUSqCxymSm/9NZkPU3ElaQ9xQuTzPpztM4ROfb8f2Yv6/ZESZsTo0MTAkp8Pcy+WkioI/uJ1H7zqs0EA4OMY4aDJRu0UtP4rTVeYNEAuRXdX+eH4aW3KMvhzpFTjMbaJHJXlEeUm2SaX5TNQyTOvghCeQILfYIL/Ca2ij8iwCmulwdV6eQGfd4VDu40PvSnmfoaE38o6HaPnX0kUcnKiT" dockerImageTagSupportsRSA1 = "8.1_p1-r0-ls20" dockerImageTagSupportsNoRSA1 = "8.4_p1-r3-ls48" ) func prepareTestContainer(t *testing.T, tag, caPublicKeyPEM string) (func(), string) { if tag == "" { tag = dockerImageTagSupportsNoRSA1 } runner, err := docker.NewServiceRunner(docker.RunOptions{ ContainerName: "openssh", ImageRepo: "docker.mirror.hashicorp.services/linuxserver/openssh-server", ImageTag: tag, Env: []string{ "DOCKER_MODS=linuxserver/mods:openssh-server-openssh-client", "PUBLIC_KEY=" + testPublicKeyInstall, "SUDO_ACCESS=true", "USER_NAME=vaultssh", }, Ports: []string{"2222/tcp"}, }) if err != nil { t.Fatalf("Could not start local ssh docker container: %s", err) } svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) { ipaddr, err := net.ResolveIPAddr("ip", host) if err != nil { return nil, err } sshAddress := fmt.Sprintf("%s:%d", ipaddr.String(), port) signer, err := ssh.ParsePrivateKey([]byte(testSharedPrivateKey)) if err != nil { return nil, err } // Install util-linux for non-busybox flock that supports timeout option err = testSSH("vaultssh", sshAddress, ssh.PublicKeys(signer), fmt.Sprintf(` set -e; sudo ln -s /config /home/vaultssh sudo apk add util-linux; echo "LogLevel DEBUG" | sudo tee -a /config/ssh_host_keys/sshd_config; echo "TrustedUserCAKeys /config/ssh_host_keys/trusted-user-ca-keys.pem" | sudo tee -a /config/ssh_host_keys/sshd_config; kill -HUP $(cat /config/sshd.pid) echo "%s" | sudo tee /config/ssh_host_keys/trusted-user-ca-keys.pem `, caPublicKeyPEM)) if err != nil { return nil, err } return docker.NewServiceHostPort(ipaddr.String(), port), nil }) if err != nil { t.Fatalf("Could not start docker ssh server: %s", err) } return svc.Cleanup, svc.Config.Address() } func testSSH(user, host string, auth ssh.AuthMethod, command string) error { client, err := ssh.Dial("tcp", host, &ssh.ClientConfig{ User: user, Auth: []ssh.AuthMethod{auth}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 5 * time.Second, }) if err != nil { return fmt.Errorf("unable to dial sshd to host %q: %v", host, err) } session, err := client.NewSession() if err != nil { return fmt.Errorf("unable to create sshd session to host %q: %v", host, err) } var stderr bytes.Buffer session.Stderr = &stderr defer session.Close() err = session.Run(command) if err != nil { return fmt.Errorf("command %v failed, error: %v, stderr: %v", command, err, stderr.String()) } return nil } func TestBackend_AllowedUsers(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Backend(config) if err != nil { t.Fatal(err) } err = b.Setup(context.Background(), config) if err != nil { t.Fatal(err) } roleData := map[string]interface{}{ "key_type": "otp", "default_user": "ubuntu", "cidr_list": "52.207.235.245/16", "allowed_users": "test", } roleReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "roles/role1", Storage: config.StorageView, Data: roleData, } resp, err := b.HandleRequest(context.Background(), roleReq) if err != nil || (resp != nil && resp.IsError()) || resp != nil { t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) } credsData := map[string]interface{}{ "ip": "52.207.235.245", "username": "ubuntu", } credsReq := &logical.Request{ Operation: logical.UpdateOperation, Storage: config.StorageView, Path: "creds/role1", Data: credsData, } resp, err = b.HandleRequest(context.Background(), credsReq) if err != nil || (resp != nil && resp.IsError()) || resp == nil { t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) } if resp.Data["key"] == "" || resp.Data["key_type"] != "otp" || resp.Data["ip"] != "52.207.235.245" || resp.Data["username"] != "ubuntu" { t.Fatalf("failed to create credential: resp:%#v", resp) } credsData["username"] = "test" resp, err = b.HandleRequest(context.Background(), credsReq) if err != nil || (resp != nil && resp.IsError()) || resp == nil { t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) } if resp.Data["key"] == "" || resp.Data["key_type"] != "otp" || resp.Data["ip"] != "52.207.235.245" || resp.Data["username"] != "test" { t.Fatalf("failed to create credential: resp:%#v", resp) } credsData["username"] = "random" resp, err = b.HandleRequest(context.Background(), credsReq) if err != nil || resp == nil || (resp != nil && !resp.IsError()) { t.Fatalf("expected failure: resp:%#v err:%s", resp, err) } delete(roleData, "allowed_users") resp, err = b.HandleRequest(context.Background(), roleReq) if err != nil || (resp != nil && resp.IsError()) || resp != nil { t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) } credsData["username"] = "ubuntu" resp, err = b.HandleRequest(context.Background(), credsReq) if err != nil || (resp != nil && resp.IsError()) || resp == nil { t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) } if resp.Data["key"] == "" || resp.Data["key_type"] != "otp" || resp.Data["ip"] != "52.207.235.245" || resp.Data["username"] != "ubuntu" { t.Fatalf("failed to create credential: resp:%#v", resp) } credsData["username"] = "test" resp, err = b.HandleRequest(context.Background(), credsReq) if err != nil || resp == nil || (resp != nil && !resp.IsError()) { t.Fatalf("expected failure: resp:%#v err:%s", resp, err) } roleData["allowed_users"] = "*" resp, err = b.HandleRequest(context.Background(), roleReq) if err != nil || (resp != nil && resp.IsError()) || resp != nil { t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) } resp, err = b.HandleRequest(context.Background(), credsReq) if err != nil || (resp != nil && resp.IsError()) || resp == nil { t.Fatalf("failed to create role: resp:%#v err:%s", resp, err) } if resp.Data["key"] == "" || resp.Data["key_type"] != "otp" || resp.Data["ip"] != "52.207.235.245" || resp.Data["username"] != "test" { t.Fatalf("failed to create credential: resp:%#v", resp) } } 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 }}", testUserName, map[string]string{ "ssh_username": testUserName, }, ) } func TestBackend_MultipleAllowedUsersTemplate(t *testing.T) { testAllowedUsersTemplate(t, "{{ identity.entity.metadata.ssh_username }}", testUserName, map[string]string{ "ssh_username": testMultiUserName, }, ) } func TestBackend_AllowedUsersTemplate_WithStaticPrefix(t *testing.T) { testAllowedUsersTemplate(t, "ssh-{{ identity.entity.metadata.ssh_username }}", "ssh-"+testUserName, map[string]string{ "ssh_username": testUserName, }, ) } func TestBackend_DefaultUserTemplate(t *testing.T) { testDefaultUserTemplate(t, "{{ identity.entity.metadata.ssh_username }}", testUserName, map[string]string{ "ssh_username": testUserName, }, ) } func TestBackend_DefaultUserTemplate_WithStaticPrefix(t *testing.T) { testDefaultUserTemplate(t, "user-{{ identity.entity.metadata.ssh_username }}", "user-"+testUserName, map[string]string{ "ssh_username": testUserName, }, ) } func TestBackend_DefaultUserTemplateFalse_AllowedUsersTemplateTrue(t *testing.T) { cluster, userpassToken := getSshCaTestCluster(t, testUserName) defer cluster.Cleanup() client := cluster.Cores[0].Client // set metadata "ssh_username" to userpass username tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{ "token": userpassToken, }) if err != nil { t.Fatal(err) } entityID := tokenLookupResponse.Data["entity_id"].(string) _, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{ "metadata": map[string]string{ "ssh_username": testUserName, }, }) if err != nil { t.Fatal(err) } _, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{ "key_type": testCaKeyType, "allow_user_certificates": true, "default_user": "{{identity.entity.metadata.ssh_username}}", // disable user templating but not allowed_user_template and the request should fail "default_user_template": false, "allowed_users": "{{identity.entity.metadata.ssh_username}}", "allowed_users_template": true, }) if err != nil { t.Fatal(err) } // sign SSH key as userpass user client.SetToken(userpassToken) _, err = client.Logical().Write("ssh/sign/my-role", map[string]interface{}{ "public_key": testCAPublicKey, }) if err == nil { t.Errorf("signing request should fail when default_user is not in the allowed_users list, because allowed_users_template is true and default_user_template is not") } expectedErrStr := "{{identity.entity.metadata.ssh_username}} is not a valid value for valid_principals" if !strings.Contains(err.Error(), expectedErrStr) { t.Errorf("expected error to include %q but it was: %q", expectedErrStr, err.Error()) } } func TestBackend_DefaultUserTemplateFalse_AllowedUsersTemplateFalse(t *testing.T) { cluster, userpassToken := getSshCaTestCluster(t, testUserName) defer cluster.Cleanup() client := cluster.Cores[0].Client // set metadata "ssh_username" to userpass username tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{ "token": userpassToken, }) if err != nil { t.Fatal(err) } entityID := tokenLookupResponse.Data["entity_id"].(string) _, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{ "metadata": map[string]string{ "ssh_username": testUserName, }, }) if err != nil { t.Fatal(err) } _, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{ "key_type": testCaKeyType, "allow_user_certificates": true, "default_user": "{{identity.entity.metadata.ssh_username}}", "default_user_template": false, "allowed_users": "{{identity.entity.metadata.ssh_username}}", "allowed_users_template": false, }) 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, }) if err != nil { t.Fatal(err) } // check for the expected valid principals of certificate signedKey := signResponse.Data["signed_key"].(string) key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) parsedKey, err := ssh.ParsePublicKey(key) if err != nil { t.Fatal(err) } actualPrincipals := parsedKey.(*ssh.Certificate).ValidPrincipals if len(actualPrincipals) < 1 { t.Fatal( fmt.Sprintf("No ValidPrincipals returned: should have been %v", []string{"{{identity.entity.metadata.ssh_username}}"}), ) } if len(actualPrincipals) > 1 { t.Error( fmt.Sprintf("incorrect number ValidPrincipals, expected only 1: %v should be %v", actualPrincipals, []string{"{{identity.entity.metadata.ssh_username}}"}), ) } if actualPrincipals[0] != "{{identity.entity.metadata.ssh_username}}" { t.Fatal( fmt.Sprintf("incorrect ValidPrincipals: %v should be %v", actualPrincipals, []string{"{{identity.entity.metadata.ssh_username}}"}), ) } } func newTestingFactory(t *testing.T) func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { return func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { defaultLeaseTTLVal := 2 * time.Minute maxLeaseTTLVal := 10 * time.Minute return Factory(context.Background(), &logical.BackendConfig{ Logger: vault.NewTestLogger(t), StorageView: &logical.InmemStorage{}, System: &logical.StaticSystemView{ DefaultLeaseTTLVal: defaultLeaseTTLVal, MaxLeaseTTLVal: maxLeaseTTLVal, }, }) } } func TestSSHBackend_Lookup(t *testing.T) { testOTPRoleData := map[string]interface{}{ "key_type": testOTPKeyType, "default_user": testUserName, "cidr_list": testCIDRList, } data := map[string]interface{}{ "ip": testIP, } resp1 := []string(nil) resp2 := []string{testOTPRoleName} resp3 := []string{testAtRoleName} logicaltest.Test(t, logicaltest.TestCase{ LogicalFactory: newTestingFactory(t), Steps: []logicaltest.TestStep{ testLookupRead(t, data, resp1), testRoleWrite(t, testOTPRoleName, testOTPRoleData), testLookupRead(t, data, resp2), testRoleDelete(t, testOTPRoleName), testLookupRead(t, data, resp1), testRoleWrite(t, testAtRoleName, testOTPRoleData), testLookupRead(t, data, resp3), testRoleDelete(t, testAtRoleName), testLookupRead(t, data, resp1), }, }) } func TestSSHBackend_RoleList(t *testing.T) { testOTPRoleData := map[string]interface{}{ "key_type": testOTPKeyType, "default_user": testUserName, "cidr_list": testCIDRList, } resp1 := map[string]interface{}{} resp2 := map[string]interface{}{ "keys": []string{testOTPRoleName}, "key_info": map[string]interface{}{ testOTPRoleName: map[string]interface{}{ "key_type": testOTPKeyType, }, }, } resp3 := map[string]interface{}{ "keys": []string{testAtRoleName, testOTPRoleName}, "key_info": map[string]interface{}{ testOTPRoleName: map[string]interface{}{ "key_type": testOTPKeyType, }, testAtRoleName: map[string]interface{}{ "key_type": testOTPKeyType, }, }, } logicaltest.Test(t, logicaltest.TestCase{ LogicalFactory: newTestingFactory(t), Steps: []logicaltest.TestStep{ testRoleList(t, resp1), testRoleWrite(t, testOTPRoleName, testOTPRoleData), testRoleList(t, resp2), testRoleWrite(t, testAtRoleName, testOTPRoleData), testRoleList(t, resp3), testRoleDelete(t, testAtRoleName), testRoleList(t, resp2), testRoleDelete(t, testOTPRoleName), testRoleList(t, resp1), }, }) } func TestSSHBackend_OTPRoleCrud(t *testing.T) { testOTPRoleData := map[string]interface{}{ "key_type": testOTPKeyType, "default_user": testUserName, "cidr_list": testCIDRList, } respOTPRoleData := map[string]interface{}{ "key_type": testOTPKeyType, "port": 22, "default_user": testUserName, "cidr_list": testCIDRList, } logicaltest.Test(t, logicaltest.TestCase{ LogicalFactory: newTestingFactory(t), Steps: []logicaltest.TestStep{ testRoleWrite(t, testOTPRoleName, testOTPRoleData), testRoleRead(t, testOTPRoleName, respOTPRoleData), testRoleDelete(t, testOTPRoleName), testRoleRead(t, testOTPRoleName, nil), testRoleWrite(t, testAtRoleName, testOTPRoleData), testRoleRead(t, testAtRoleName, respOTPRoleData), testRoleDelete(t, testAtRoleName), testRoleRead(t, testAtRoleName, nil), }, }) } func TestSSHBackend_OTPCreate(t *testing.T) { cleanup, sshAddress := prepareTestContainer(t, "", "") defer func() { if !t.Failed() { cleanup() } }() host, port, err := net.SplitHostPort(sshAddress) if err != nil { t.Fatal(err) } testOTPRoleData := map[string]interface{}{ "key_type": testOTPKeyType, "default_user": testUserName, "cidr_list": host + "/32", "port": port, } data := map[string]interface{}{ "username": testUserName, "ip": host, } logicaltest.Test(t, logicaltest.TestCase{ LogicalFactory: newTestingFactory(t), Steps: []logicaltest.TestStep{ testRoleWrite(t, testOTPRoleName, testOTPRoleData), testCredsWrite(t, testOTPRoleName, data, false, sshAddress), }, }) } func TestSSHBackend_VerifyEcho(t *testing.T) { verifyData := map[string]interface{}{ "otp": api.VerifyEchoRequest, } expectedData := map[string]interface{}{ "message": api.VerifyEchoResponse, } logicaltest.Test(t, logicaltest.TestCase{ LogicalFactory: newTestingFactory(t), Steps: []logicaltest.TestStep{ testVerifyWrite(t, verifyData, expectedData), }, }) } func TestSSHBackend_ConfigZeroAddressCRUD(t *testing.T) { testOTPRoleData := map[string]interface{}{ "key_type": testOTPKeyType, "default_user": testUserName, "cidr_list": testCIDRList, } req1 := map[string]interface{}{ "roles": testOTPRoleName, } resp1 := map[string]interface{}{ "roles": []string{testOTPRoleName}, } resp2 := map[string]interface{}{ "roles": []string{testOTPRoleName}, } resp3 := map[string]interface{}{ "roles": []string{}, } logicaltest.Test(t, logicaltest.TestCase{ LogicalFactory: newTestingFactory(t), Steps: []logicaltest.TestStep{ testRoleWrite(t, testOTPRoleName, testOTPRoleData), testConfigZeroAddressWrite(t, req1), testConfigZeroAddressRead(t, resp1), testConfigZeroAddressRead(t, resp2), testConfigZeroAddressRead(t, resp1), testRoleDelete(t, testOTPRoleName), testConfigZeroAddressRead(t, resp3), testConfigZeroAddressDelete(t), }, }) } func TestSSHBackend_CredsForZeroAddressRoles_otp(t *testing.T) { otpRoleData := map[string]interface{}{ "key_type": testOTPKeyType, "default_user": testUserName, } data := map[string]interface{}{ "username": testUserName, "ip": testIP, } req1 := map[string]interface{}{ "roles": testOTPRoleName, } logicaltest.Test(t, logicaltest.TestCase{ LogicalFactory: newTestingFactory(t), Steps: []logicaltest.TestStep{ testRoleWrite(t, testOTPRoleName, otpRoleData), testCredsWrite(t, testOTPRoleName, data, true, ""), testConfigZeroAddressWrite(t, req1), testCredsWrite(t, testOTPRoleName, data, false, ""), testConfigZeroAddressDelete(t), testCredsWrite(t, testOTPRoleName, data, true, ""), }, }) } func TestSSHBackend_CA(t *testing.T) { testCases := []struct { name string tag string caPublicKey string caPrivateKey string algoSigner string expectError bool }{ { "RSAKey_EmptyAlgoSigner_ImageSupportsRSA1", dockerImageTagSupportsRSA1, testCAPublicKey, testCAPrivateKey, "", false, }, { "RSAKey_EmptyAlgoSigner_ImageSupportsNoRSA1", dockerImageTagSupportsNoRSA1, testCAPublicKey, testCAPrivateKey, "", false, }, { "RSAKey_DefaultAlgoSigner_ImageSupportsRSA1", dockerImageTagSupportsRSA1, testCAPublicKey, testCAPrivateKey, "default", false, }, { "RSAKey_DefaultAlgoSigner_ImageSupportsNoRSA1", dockerImageTagSupportsNoRSA1, testCAPublicKey, testCAPrivateKey, "default", false, }, { "RSAKey_RSA1AlgoSigner_ImageSupportsRSA1", dockerImageTagSupportsRSA1, testCAPublicKey, testCAPrivateKey, ssh.SigAlgoRSA, false, }, { "RSAKey_RSA1AlgoSigner_ImageSupportsNoRSA1", dockerImageTagSupportsNoRSA1, testCAPublicKey, testCAPrivateKey, ssh.SigAlgoRSA, true, }, { "RSAKey_RSASHA2256AlgoSigner_ImageSupportsRSA1", dockerImageTagSupportsRSA1, testCAPublicKey, testCAPrivateKey, ssh.SigAlgoRSASHA2256, false, }, { "RSAKey_RSASHA2256AlgoSigner_ImageSupportsNoRSA1", dockerImageTagSupportsNoRSA1, testCAPublicKey, testCAPrivateKey, ssh.SigAlgoRSASHA2256, false, }, { "ed25519Key_EmptyAlgoSigner_ImageSupportsRSA1", dockerImageTagSupportsRSA1, testCAPublicKeyEd25519, testCAPrivateKeyEd25519, "", false, }, { "ed25519Key_EmptyAlgoSigner_ImageSupportsNoRSA1", dockerImageTagSupportsNoRSA1, testCAPublicKeyEd25519, testCAPrivateKeyEd25519, "", false, }, { "ed25519Key_RSA1AlgoSigner_ImageSupportsRSA1", dockerImageTagSupportsRSA1, testCAPublicKeyEd25519, testCAPrivateKeyEd25519, ssh.SigAlgoRSA, true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { testSSHBackend_CA(t, tc.tag, tc.caPublicKey, tc.caPrivateKey, tc.algoSigner, tc.expectError) }) } } func testSSHBackend_CA(t *testing.T, dockerImageTag, caPublicKey, caPrivateKey, algorithmSigner string, expectError bool) { cleanup, sshAddress := prepareTestContainer(t, dockerImageTag, caPublicKey) defer cleanup() config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testKeyToSignPrivate := `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn NhAAAAAwEAAQAAAQEAwn1V2xd/EgJXIY53fBTtc20k/ajekqQngvkpFSwNHW63XNEQK8Ll FOCyGXoje9DUGxnYs3F/ohfsBBWkLNfU7fiENdSJL1pbkAgJ+2uhV9sLZjvYhikrXWoyJX LDKfY12LjpcBS2HeLMT04laZ/xSJrOBEJHGzHyr2wUO0NUQUQPUODAFhnHKgvvA4Uu79UY gcdThF4w83+EAnE4JzBZMKPMjzy4u1C0R/LoD8DuapHwX6NGWdEUvUZZ+XRcIWeCOvR0ne qGBRH35k1Mv7k65d7kkE0uvM5Z36erw3tdoszxPYf7AKnO1DpeU2uwMcym6xNwfwynKjhL qL/Mgi4uRwAAA8iAsY0zgLGNMwAAAAdzc2gtcnNhAAABAQDCfVXbF38SAlchjnd8FO1zbS T9qN6SpCeC+SkVLA0dbrdc0RArwuUU4LIZeiN70NQbGdizcX+iF+wEFaQs19Tt+IQ11Ikv WluQCAn7a6FX2wtmO9iGKStdajIlcsMp9jXYuOlwFLYd4sxPTiVpn/FIms4EQkcbMfKvbB Q7Q1RBRA9Q4MAWGccqC+8DhS7v1RiBx1OEXjDzf4QCcTgnMFkwo8yPPLi7ULRH8ugPwO5q kfBfo0ZZ0RS9Rln5dFwhZ4I69HSd6oYFEffmTUy/uTrl3uSQTS68zlnfp6vDe12izPE9h/ sAqc7UOl5Ta7AxzKbrE3B/DKcqOEuov8yCLi5HAAAAAwEAAQAAAQABns2yT5XNbpuPOgKg 1APObGBchKWmDxwNKUpAVOefEScR7OP3mV4TOHQDZlMZWvoJZ8O4av+nOA/NUOjXPs0VVn azhBvIezY8EvUSVSk49Cg6J9F7/KfR1WqpiTU7CkQUlCXNuz5xLUyKdJo3MQ/vjOqeenbh MR9Wes4IWF1BVe4VOD6lxRsjwuIieIgmScW28FFh2rgsEfO2spzZ3AWOGExw+ih757hFz5 4A2fhsQXP8m3r8m7iiqcjTLWXdxTUk4zot2kZEjbI4Avk0BL+wVeFq6f/y+G+g5edqSo7j uuSgzbUQtA9PMnGxhrhU2Ob7n3VGdya7WbGZkaKP8zJhAAAAgQC3bJurmOSLIi3KVhp7lD /FfxwXHwVBFALCgq7EyNlkTz6RDoMFM4eOTRMDvsgWxT+bSB8R8eg1sfgY8rkHOuvTAVI5 3oEYco3H7NWE9X8Zt0lyhO1uaE49EENNSQ8hY7R3UIw5becyI+7ZZxs9HkBgCQCZzSjzA+ SIyAoMKM261AAAAIEA+PCkcDRp3J0PaoiuetXSlWZ5WjP3CtwT2xrvEX9x+ZsDgXCDYQ5T osxvEKOGSfIrHUUhzZbFGvqWyfrziPe9ypJrtCM7RJT/fApBXnbWFcDZzWamkQvohst+0w XHYCmNoJ6/Y+roLv3pzyFUmqRNcrQaohex7TZmsvHJT513UakAAACBAMgBXxH8DyNYdniX mIXEto4GqMh4rXdNwCghfpyWdJE6vCyDt7g7bYMq7AQ2ynSKRtQDT/ZgQNfSbilUq3iXz7 xNZn5U9ndwFs90VmEpBup/PmhfX+Gwt5hQZLbkKZcgQ9XrhSKdMxVm1yy/fk0U457enlz5 cKumubUxOfFdy1ZvAAAAEm5jY0BtYnAudWJudC5sb2NhbA== -----END OPENSSH PRIVATE KEY----- ` testKeyToSignPublic := `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCfVXbF38SAlchjnd8FO1zbST9qN6SpCeC+SkVLA0dbrdc0RArwuUU4LIZeiN70NQbGdizcX+iF+wEFaQs19Tt+IQ11IkvWluQCAn7a6FX2wtmO9iGKStdajIlcsMp9jXYuOlwFLYd4sxPTiVpn/FIms4EQkcbMfKvbBQ7Q1RBRA9Q4MAWGccqC+8DhS7v1RiBx1OEXjDzf4QCcTgnMFkwo8yPPLi7ULRH8ugPwO5qkfBfo0ZZ0RS9Rln5dFwhZ4I69HSd6oYFEffmTUy/uTrl3uSQTS68zlnfp6vDe12izPE9h/sAqc7UOl5Ta7AxzKbrE3B/DKcqOEuov8yCLi5H ` roleOptions := map[string]interface{}{ "allow_user_certificates": true, "allowed_users": "*", "default_extensions": []map[string]string{ { "permit-pty": "", }, }, "key_type": "ca", "default_user": testUserName, "ttl": "30m0s", } if algorithmSigner != "" { roleOptions["algorithm_signer"] = algorithmSigner } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(caPublicKey, caPrivateKey), testRoleWrite(t, "testcarole", roleOptions), { Operation: logical.UpdateOperation, Path: "sign/testcarole", ErrorOk: expectError, Data: map[string]interface{}{ "public_key": testKeyToSignPublic, "valid_principals": testUserName, }, Check: func(resp *logical.Response) error { // Tolerate nil response if an error was expected if expectError && resp == nil { return nil } signedKey := strings.TrimSpace(resp.Data["signed_key"].(string)) if signedKey == "" { return errors.New("no signed key in response") } privKey, err := ssh.ParsePrivateKey([]byte(testKeyToSignPrivate)) if err != nil { return fmt.Errorf("error parsing private key: %v", err) } parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(signedKey)) if err != nil { return fmt.Errorf("error parsing signed key: %v", err) } certSigner, err := ssh.NewCertSigner(parsedKey.(*ssh.Certificate), privKey) if err != nil { return err } err = testSSH(testUserName, sshAddress, ssh.PublicKeys(certSigner), "date") if expectError && err == nil { return fmt.Errorf("expected error but got none") } if !expectError && err != nil { return err } return nil }, }, }, } logicaltest.Test(t, testCase) } func TestSSHBackend_CAUpgradeAlgorithmSigner(t *testing.T) { cleanup, sshAddress := prepareTestContainer(t, dockerImageTagSupportsRSA1, testCAPublicKey) defer cleanup() config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testKeyToSignPrivate := `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn NhAAAAAwEAAQAAAQEAwn1V2xd/EgJXIY53fBTtc20k/ajekqQngvkpFSwNHW63XNEQK8Ll FOCyGXoje9DUGxnYs3F/ohfsBBWkLNfU7fiENdSJL1pbkAgJ+2uhV9sLZjvYhikrXWoyJX LDKfY12LjpcBS2HeLMT04laZ/xSJrOBEJHGzHyr2wUO0NUQUQPUODAFhnHKgvvA4Uu79UY gcdThF4w83+EAnE4JzBZMKPMjzy4u1C0R/LoD8DuapHwX6NGWdEUvUZZ+XRcIWeCOvR0ne qGBRH35k1Mv7k65d7kkE0uvM5Z36erw3tdoszxPYf7AKnO1DpeU2uwMcym6xNwfwynKjhL qL/Mgi4uRwAAA8iAsY0zgLGNMwAAAAdzc2gtcnNhAAABAQDCfVXbF38SAlchjnd8FO1zbS T9qN6SpCeC+SkVLA0dbrdc0RArwuUU4LIZeiN70NQbGdizcX+iF+wEFaQs19Tt+IQ11Ikv WluQCAn7a6FX2wtmO9iGKStdajIlcsMp9jXYuOlwFLYd4sxPTiVpn/FIms4EQkcbMfKvbB Q7Q1RBRA9Q4MAWGccqC+8DhS7v1RiBx1OEXjDzf4QCcTgnMFkwo8yPPLi7ULRH8ugPwO5q kfBfo0ZZ0RS9Rln5dFwhZ4I69HSd6oYFEffmTUy/uTrl3uSQTS68zlnfp6vDe12izPE9h/ sAqc7UOl5Ta7AxzKbrE3B/DKcqOEuov8yCLi5HAAAAAwEAAQAAAQABns2yT5XNbpuPOgKg 1APObGBchKWmDxwNKUpAVOefEScR7OP3mV4TOHQDZlMZWvoJZ8O4av+nOA/NUOjXPs0VVn azhBvIezY8EvUSVSk49Cg6J9F7/KfR1WqpiTU7CkQUlCXNuz5xLUyKdJo3MQ/vjOqeenbh MR9Wes4IWF1BVe4VOD6lxRsjwuIieIgmScW28FFh2rgsEfO2spzZ3AWOGExw+ih757hFz5 4A2fhsQXP8m3r8m7iiqcjTLWXdxTUk4zot2kZEjbI4Avk0BL+wVeFq6f/y+G+g5edqSo7j uuSgzbUQtA9PMnGxhrhU2Ob7n3VGdya7WbGZkaKP8zJhAAAAgQC3bJurmOSLIi3KVhp7lD /FfxwXHwVBFALCgq7EyNlkTz6RDoMFM4eOTRMDvsgWxT+bSB8R8eg1sfgY8rkHOuvTAVI5 3oEYco3H7NWE9X8Zt0lyhO1uaE49EENNSQ8hY7R3UIw5becyI+7ZZxs9HkBgCQCZzSjzA+ SIyAoMKM261AAAAIEA+PCkcDRp3J0PaoiuetXSlWZ5WjP3CtwT2xrvEX9x+ZsDgXCDYQ5T osxvEKOGSfIrHUUhzZbFGvqWyfrziPe9ypJrtCM7RJT/fApBXnbWFcDZzWamkQvohst+0w XHYCmNoJ6/Y+roLv3pzyFUmqRNcrQaohex7TZmsvHJT513UakAAACBAMgBXxH8DyNYdniX mIXEto4GqMh4rXdNwCghfpyWdJE6vCyDt7g7bYMq7AQ2ynSKRtQDT/ZgQNfSbilUq3iXz7 xNZn5U9ndwFs90VmEpBup/PmhfX+Gwt5hQZLbkKZcgQ9XrhSKdMxVm1yy/fk0U457enlz5 cKumubUxOfFdy1ZvAAAAEm5jY0BtYnAudWJudC5sb2NhbA== -----END OPENSSH PRIVATE KEY----- ` testKeyToSignPublic := `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCfVXbF38SAlchjnd8FO1zbST9qN6SpCeC+SkVLA0dbrdc0RArwuUU4LIZeiN70NQbGdizcX+iF+wEFaQs19Tt+IQ11IkvWluQCAn7a6FX2wtmO9iGKStdajIlcsMp9jXYuOlwFLYd4sxPTiVpn/FIms4EQkcbMfKvbBQ7Q1RBRA9Q4MAWGccqC+8DhS7v1RiBx1OEXjDzf4QCcTgnMFkwo8yPPLi7ULRH8ugPwO5qkfBfo0ZZ0RS9Rln5dFwhZ4I69HSd6oYFEffmTUy/uTrl3uSQTS68zlnfp6vDe12izPE9h/sAqc7UOl5Ta7AxzKbrE3B/DKcqOEuov8yCLi5H ` // Old role entries between 1.4.3 and 1.5.2 had algorithm_signer default to // ssh-rsa if not provided. roleOptionsOldEntry := map[string]interface{}{ "allow_user_certificates": true, "allowed_users": "*", "default_extensions": []map[string]string{ { "permit-pty": "", }, }, "key_type": "ca", "default_user": testUserName, "ttl": "30m0s", "algorithm_signer": ssh.SigAlgoRSA, } // Upgrade entry by overwriting algorithm_signer with an empty value roleOptionsUpgradedEntry := map[string]interface{}{ "allow_user_certificates": true, "allowed_users": "*", "default_extensions": []map[string]string{ { "permit-pty": "", }, }, "key_type": "ca", "default_user": testUserName, "ttl": "30m0s", "algorithm_signer": "", } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(testCAPublicKey, testCAPrivateKey), testRoleWrite(t, "testcarole", roleOptionsOldEntry), testRoleWrite(t, "testcarole", roleOptionsUpgradedEntry), { Operation: logical.UpdateOperation, Path: "sign/testcarole", ErrorOk: false, Data: map[string]interface{}{ "public_key": testKeyToSignPublic, "valid_principals": testUserName, }, Check: func(resp *logical.Response) error { signedKey := strings.TrimSpace(resp.Data["signed_key"].(string)) if signedKey == "" { return errors.New("no signed key in response") } privKey, err := ssh.ParsePrivateKey([]byte(testKeyToSignPrivate)) if err != nil { return fmt.Errorf("error parsing private key: %v", err) } parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(signedKey)) if err != nil { return fmt.Errorf("error parsing signed key: %v", err) } certSigner, err := ssh.NewCertSigner(parsedKey.(*ssh.Certificate), privKey) if err != nil { return err } err = testSSH(testUserName, sshAddress, ssh.PublicKeys(certSigner), "date") if err != nil { return err } return nil }, }, }, } logicaltest.Test(t, testCase) } func TestBackend_AbleToRetrievePublicKey(t *testing.T) { config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(testCAPublicKey, testCAPrivateKey), { Operation: logical.ReadOperation, Path: "public_key", Unauthenticated: true, Check: func(resp *logical.Response) error { key := string(resp.Data["http_raw_body"].([]byte)) if key != testCAPublicKey { return fmt.Errorf("public_key incorrect. Expected %v, actual %v", testCAPublicKey, key) } return nil }, }, }, } logicaltest.Test(t, testCase) } func TestBackend_AbleToAutoGenerateSigningKeys(t *testing.T) { config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } var expectedPublicKey string testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ { Operation: logical.UpdateOperation, Path: "config/ca", Check: func(resp *logical.Response) error { if resp.Data["public_key"].(string) == "" { return fmt.Errorf("public_key empty") } expectedPublicKey = resp.Data["public_key"].(string) return nil }, }, { Operation: logical.ReadOperation, Path: "public_key", Unauthenticated: true, Check: func(resp *logical.Response) error { key := string(resp.Data["http_raw_body"].([]byte)) if key == "" { return fmt.Errorf("public_key empty. Expected not empty, actual %s", key) } if key != expectedPublicKey { return fmt.Errorf("public_key mismatch. Expected %s, actual %s", expectedPublicKey, key) } return nil }, }, }, } logicaltest.Test(t, testCase) } func TestBackend_ValidPrincipalsValidatedForHostCertificates(t *testing.T) { config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(testCAPublicKey, testCAPrivateKey), createRoleStep("testing", map[string]interface{}{ "key_type": "ca", "allow_host_certificates": true, "allowed_domains": "example.com,example.org", "allow_subdomains": true, "default_critical_options": map[string]interface{}{ "option": "value", }, "default_extensions": map[string]interface{}{ "extension": "extended", }, }), signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.HostCert, []string{"dummy.example.org", "second.example.com"}, map[string]string{ "option": "value", }, map[string]string{ "extension": "extended", }, 2*time.Hour, map[string]interface{}{ "public_key": publicKey2, "ttl": "2h", "cert_type": "host", "valid_principals": "dummy.example.org,second.example.com", }), }, } logicaltest.Test(t, testCase) } func TestBackend_OptionsOverrideDefaults(t *testing.T) { config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(testCAPublicKey, testCAPrivateKey), createRoleStep("testing", map[string]interface{}{ "key_type": "ca", "allowed_users": "tuber", "default_user": "tuber", "allow_user_certificates": true, "allowed_critical_options": "option,secondary", "allowed_extensions": "extension,additional", "default_critical_options": map[string]interface{}{ "option": "value", }, "default_extensions": map[string]interface{}{ "extension": "extended", }, }), signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.UserCert, []string{"tuber"}, map[string]string{ "secondary": "value", }, map[string]string{ "additional": "value", }, 2*time.Hour, map[string]interface{}{ "public_key": publicKey2, "ttl": "2h", "critical_options": map[string]interface{}{ "secondary": "value", }, "extensions": map[string]interface{}{ "additional": "value", }, }), }, } logicaltest.Test(t, testCase) } func TestBackend_AllowedUserKeyLengths(t *testing.T) { config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(testCAPublicKey, testCAPrivateKey), createRoleStep("weakkey", map[string]interface{}{ "key_type": "ca", "allow_user_certificates": true, "allowed_user_key_lengths": map[string]interface{}{ "rsa": 4096, }, }), { Operation: logical.UpdateOperation, Path: "sign/weakkey", Data: map[string]interface{}{ "public_key": testCAPublicKey, }, ErrorOk: true, Check: func(resp *logical.Response) error { if resp.Data["error"] != "public_key failed to meet the key requirements: key is of an invalid size: 2048" { return errors.New("a smaller key (2048) was allowed, when the minimum was set for 4096") } return nil }, }, createRoleStep("stdkey", map[string]interface{}{ "key_type": "ca", "allow_user_certificates": true, "allowed_user_key_lengths": map[string]interface{}{ "rsa": 2048, }, }), // Pass with 2048 key { Operation: logical.UpdateOperation, Path: "sign/stdkey", Data: map[string]interface{}{ "public_key": testCAPublicKey, }, }, // Fail with 4096 key { Operation: logical.UpdateOperation, Path: "sign/stdkey", Data: map[string]interface{}{ "public_key": publicKey4096, }, ErrorOk: true, Check: func(resp *logical.Response) error { if resp.Data["error"] != "public_key failed to meet the key requirements: key is of an invalid size: 4096" { return errors.New("a larger key (4096) was allowed, when the size was set for 2048") } return nil }, }, createRoleStep("multikey", map[string]interface{}{ "key_type": "ca", "allow_user_certificates": true, "allowed_user_key_lengths": map[string]interface{}{ "rsa": []int{2048, 4096}, }, }), // Pass with 2048-bit key { Operation: logical.UpdateOperation, Path: "sign/multikey", Data: map[string]interface{}{ "public_key": testCAPublicKey, }, }, // Pass with 4096-bit key { Operation: logical.UpdateOperation, Path: "sign/multikey", Data: map[string]interface{}{ "public_key": publicKey4096, }, }, // Fail with 3072-bit key { Operation: logical.UpdateOperation, Path: "sign/multikey", Data: map[string]interface{}{ "public_key": publicKey3072, }, ErrorOk: true, Check: func(resp *logical.Response) error { if resp.Data["error"] != "public_key failed to meet the key requirements: key is of an invalid size: 3072" { return errors.New("a larger key (3072) was allowed, when the size was set for 2048") } return nil }, }, // Fail with ECDSA key { Operation: logical.UpdateOperation, Path: "sign/multikey", Data: map[string]interface{}{ "public_key": publicKeyECDSA256, }, ErrorOk: true, Check: func(resp *logical.Response) error { if resp.Data["error"] != "public_key failed to meet the key requirements: key of type ecdsa is not allowed" { return errors.New("an ECDSA key was allowed under RSA-only policy") } return nil }, }, createRoleStep("ectypes", map[string]interface{}{ "key_type": "ca", "allow_user_certificates": true, "allowed_user_key_lengths": map[string]interface{}{ "ec": []int{256}, "ecdsa-sha2-nistp521": 0, }, }), // Pass with ECDSA P-256 { Operation: logical.UpdateOperation, Path: "sign/ectypes", Data: map[string]interface{}{ "public_key": publicKeyECDSA256, }, }, // Pass with ECDSA P-521 { Operation: logical.UpdateOperation, Path: "sign/ectypes", Data: map[string]interface{}{ "public_key": publicKeyECDSA521, }, }, // Fail with RSA key { Operation: logical.UpdateOperation, Path: "sign/ectypes", Data: map[string]interface{}{ "public_key": publicKey3072, }, ErrorOk: true, Check: func(resp *logical.Response) error { if resp.Data["error"] != "public_key failed to meet the key requirements: key of type rsa is not allowed" { return errors.New("an RSA key was allowed under ECDSA-only policy") } return nil }, }, }, } logicaltest.Test(t, testCase) } func TestBackend_CustomKeyIDFormat(t *testing.T) { config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(testCAPublicKey, testCAPrivateKey), createRoleStep("customrole", map[string]interface{}{ "key_type": "ca", "key_id_format": "{{role_name}}-{{token_display_name}}-{{public_key_hash}}", "allowed_users": "tuber", "default_user": "tuber", "allow_user_certificates": true, "allowed_critical_options": "option,secondary", "allowed_extensions": "extension,additional", "default_critical_options": map[string]interface{}{ "option": "value", }, "default_extensions": map[string]interface{}{ "extension": "extended", }, }), signCertificateStep("customrole", "customrole-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.UserCert, []string{"tuber"}, map[string]string{ "secondary": "value", }, map[string]string{ "additional": "value", }, 2*time.Hour, map[string]interface{}{ "public_key": publicKey2, "ttl": "2h", "critical_options": map[string]interface{}{ "secondary": "value", }, "extensions": map[string]interface{}{ "additional": "value", }, }), }, } logicaltest.Test(t, testCase) } func TestBackend_DisallowUserProvidedKeyIDs(t *testing.T) { config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(testCAPublicKey, testCAPrivateKey), createRoleStep("testing", map[string]interface{}{ "key_type": "ca", "allow_user_key_ids": false, "allow_user_certificates": true, }), { Operation: logical.UpdateOperation, Path: "sign/testing", Data: map[string]interface{}{ "public_key": publicKey2, "key_id": "override", }, ErrorOk: true, Check: func(resp *logical.Response) error { if resp.Data["error"] != "setting key_id is not allowed by role" { return errors.New("custom user key id was allowed even when 'allow_user_key_ids' is false") } return nil }, }, }, } 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}}", "login@foobar2.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}, " + "{{identity.entity.aliases." + userpassAccessor + ".name}}_foobar", }, }) 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, "login@foobar2.com": fmt.Sprintf("%s, %s_foobar", testUserName, 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_EmptyAllowedExtensionFailsClosed(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 no allowed 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", "allowed_extensions": "", "default_extensions_template": false, "default_extensions": map[string]interface{}{ "login@foobar.com": "{{identity.entity.aliases." + userpassAccessor + ".name}}", }, }) 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", } _, err = client.Logical().Write("ssh/sign/test_allow_all_extensions", map[string]interface{}{ "public_key": publicKey4096, "extensions": userProvidedAnyExtensionPermissions, }) if err == nil { t.Fatal("Expected failure we should not have allowed specifying custom extensions") } if !strings.Contains(err.Error(), "are not on allowed list") { t.Fatalf("Expected failure to contain 'are not on allowed list' but was %s", err) } } 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", "allowed_extensions": "*", "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 TestSSHBackend_ValidateNotBeforeDuration(t *testing.T) { config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(testCAPublicKey, testCAPrivateKey), createRoleStep("testing", map[string]interface{}{ "key_type": "ca", "allow_host_certificates": true, "allowed_domains": "example.com,example.org", "allow_subdomains": true, "default_critical_options": map[string]interface{}{ "option": "value", }, "default_extensions": map[string]interface{}{ "extension": "extended", }, "not_before_duration": "300s", }), signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.HostCert, []string{"dummy.example.org", "second.example.com"}, map[string]string{ "option": "value", }, map[string]string{ "extension": "extended", }, 2*time.Hour+5*time.Minute-30*time.Second, map[string]interface{}{ "public_key": publicKey2, "ttl": "2h", "cert_type": "host", "valid_principals": "dummy.example.org,second.example.com", }), createRoleStep("testing", map[string]interface{}{ "key_type": "ca", "allow_host_certificates": true, "allowed_domains": "example.com,example.org", "allow_subdomains": true, "default_critical_options": map[string]interface{}{ "option": "value", }, "default_extensions": map[string]interface{}{ "extension": "extended", }, "not_before_duration": "2h", }), signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.HostCert, []string{"dummy.example.org", "second.example.com"}, map[string]string{ "option": "value", }, map[string]string{ "extension": "extended", }, 4*time.Hour-30*time.Second, map[string]interface{}{ "public_key": publicKey2, "ttl": "2h", "cert_type": "host", "valid_principals": "dummy.example.org,second.example.com", }), createRoleStep("testing", map[string]interface{}{ "key_type": "ca", "allow_host_certificates": true, "allowed_domains": "example.com,example.org", "allow_subdomains": true, "default_critical_options": map[string]interface{}{ "option": "value", }, "default_extensions": map[string]interface{}{ "extension": "extended", }, "not_before_duration": "30s", }), signCertificateStep("testing", "vault-root-22608f5ef173aabf700797cb95c5641e792698ec6380e8e1eb55523e39aa5e51", ssh.HostCert, []string{"dummy.example.org", "second.example.com"}, map[string]string{ "option": "value", }, map[string]string{ "extension": "extended", }, 2*time.Hour, map[string]interface{}{ "public_key": publicKey2, "ttl": "2h", "cert_type": "host", "valid_principals": "dummy.example.org,second.example.com", }), }, } logicaltest.Test(t, testCase) } func TestSSHBackend_IssueSign(t *testing.T) { config := logical.TestBackendConfig() b, err := Factory(context.Background(), config) if err != nil { t.Fatalf("Cannot create backend: %s", err) } testCase := logicaltest.TestCase{ LogicalBackend: b, Steps: []logicaltest.TestStep{ configCaStep(testCAPublicKey, testCAPrivateKey), createRoleStep("testing", map[string]interface{}{ "key_type": "otp", "default_user": "user", }), // Key pair not issued with invalid role key type issueSSHKeyPairStep("testing", "rsa", 0, true, "role key type 'otp' not allowed to issue key pairs"), createRoleStep("testing", map[string]interface{}{ "key_type": "ca", "allow_user_key_ids": false, "allow_user_certificates": true, "allowed_user_key_lengths": map[string]interface{}{ "ssh-rsa": []int{2048, 3072, 4096}, "ecdsa-sha2-nistp521": 0, "ed25519": 0, }, }), // Key_type not in allowed_user_key_types_lengths issueSSHKeyPairStep("testing", "ec", 256, true, "provided key_type value not in allowed_user_key_types"), // Key_bits not in allowed_user_key_types_lengths for provided key_type issueSSHKeyPairStep("testing", "rsa", 2560, true, "provided key_bits value not in list of role's allowed_user_key_types"), // key_type `rsa` and key_bits `2048` successfully created issueSSHKeyPairStep("testing", "rsa", 2048, false, ""), // key_type `ed22519` and key_bits `0` successfully created issueSSHKeyPairStep("testing", "ed25519", 0, false, ""), }, } logicaltest.Test(t, testCase) } 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 testDefaultUserTemplate(t *testing.T, testDefaultUserTemplate string, expectedValidPrincipal string, testEntityMetadata map[string]string, ) { cluster, userpassToken := getSshCaTestCluster(t, testUserName) defer cluster.Cleanup() client := cluster.Cores[0].Client // set metadata "ssh_username" to userpass username tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{ "token": userpassToken, }) if err != nil { t.Fatal(err) } entityID := tokenLookupResponse.Data["entity_id"].(string) _, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{ "metadata": testEntityMetadata, }) if err != nil { t.Fatal(err) } _, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{ "key_type": testCaKeyType, "allow_user_certificates": true, "default_user": testDefaultUserTemplate, "default_user_template": true, "allowed_users": testDefaultUserTemplate, "allowed_users_template": true, }) 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, }) if err != nil { t.Fatal(err) } // check for the expected valid principals of certificate signedKey := signResponse.Data["signed_key"].(string) key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) parsedKey, err := ssh.ParsePublicKey(key) if err != nil { t.Fatal(err) } actualPrincipals := parsedKey.(*ssh.Certificate).ValidPrincipals if actualPrincipals[0] != expectedValidPrincipal { t.Fatal( fmt.Sprintf("incorrect ValidPrincipals: %v should be %v", actualPrincipals, []string{expectedValidPrincipal}), ) } } 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 // set metadata "ssh_username" to userpass username tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{ "token": userpassToken, }) if err != nil { t.Fatal(err) } entityID := tokenLookupResponse.Data["entity_id"].(string) _, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{ "metadata": testEntityMetadata, }) if err != nil { t.Fatal(err) } _, 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", signingPayload) if err != nil { t.Fatal(err) } // check for the expected valid principals of certificate signedKey := signResponse.Data["signed_key"].(string) key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) parsedKey, err := ssh.ParsePublicKey(key) if err != nil { t.Fatal(err) } actualPrincipals := parsedKey.(*ssh.Certificate).ValidPrincipals if actualPrincipals[0] != expectedValidPrincipal { t.Fatal( fmt.Sprintf("incorrect ValidPrincipals: %v should be %v", actualPrincipals, []string{expectedValidPrincipal}), ) } } 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, Path: "config/ca", Data: map[string]interface{}{ "public_key": caPublicKey, "private_key": caPrivateKey, }, } } func createRoleStep(name string, parameters map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.CreateOperation, Path: "roles/" + name, Data: parameters, } } func signCertificateStep( role, keyID string, certType int, validPrincipals []string, criticalOptionPermissions, extensionPermissions map[string]string, ttl time.Duration, requestParameters map[string]interface{}, ) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "sign/" + role, Data: requestParameters, Check: func(resp *logical.Response) error { serialNumber := resp.Data["serial_number"].(string) if serialNumber == "" { return errors.New("no serial number in response") } signedKey := strings.TrimSpace(resp.Data["signed_key"].(string)) if signedKey == "" { return errors.New("no signed key in response") } key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1]) parsedKey, err := ssh.ParsePublicKey(key) if err != nil { return err } return validateSSHCertificate(parsedKey.(*ssh.Certificate), keyID, certType, validPrincipals, criticalOptionPermissions, extensionPermissions, ttl) }, } } func issueSSHKeyPairStep(role, keyType string, keyBits int, expectError bool, errorMsg string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "issue/" + role, Data: map[string]interface{}{ "key_type": keyType, "key_bits": keyBits, }, ErrorOk: true, Check: func(resp *logical.Response) error { if expectError { var err error if resp.Data["error"] != errorMsg { err = fmt.Errorf("actual error message \"%s\" different from expected error message \"%s\"", resp.Data["error"], errorMsg) } return err } if resp.IsError() { return fmt.Errorf("unexpected error response returned: %v", resp.Error()) } if resp.Data["private_key_type"] != keyType { return fmt.Errorf("response private_key_type (%s) does not match the provided key_type (%s)", resp.Data["private_key_type"], keyType) } if resp.Data["signed_key"] == "" { return errors.New("certificate/signed_key should not be empty") } return nil }, } } func validateSSHCertificate(cert *ssh.Certificate, keyID string, certType int, validPrincipals []string, criticalOptionPermissions, extensionPermissions map[string]string, ttl time.Duration, ) error { if cert.KeyId != keyID { return fmt.Errorf("incorrect KeyId: %v, wanted %v", cert.KeyId, keyID) } if cert.CertType != uint32(certType) { return fmt.Errorf("incorrect CertType: %v", cert.CertType) } if time.Unix(int64(cert.ValidAfter), 0).After(time.Now()) { return fmt.Errorf("incorrect ValidAfter: %v", cert.ValidAfter) } if time.Unix(int64(cert.ValidBefore), 0).Before(time.Now()) { return fmt.Errorf("incorrect ValidBefore: %v", cert.ValidBefore) } 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, actual %v", ttl, actualTTL) } if !reflect.DeepEqual(cert.ValidPrincipals, validPrincipals) { return fmt.Errorf("incorrect ValidPrincipals: expected: %#v actual: %#v", validPrincipals, cert.ValidPrincipals) } publicSigningKey, err := getSigningPublicKey() if err != nil { return err } if !reflect.DeepEqual(cert.SignatureKey, publicSigningKey) { return fmt.Errorf("incorrect SignatureKey: %v", cert.SignatureKey) } if cert.Signature == nil { return fmt.Errorf("incorrect Signature: %v", cert.Signature) } if !reflect.DeepEqual(cert.Permissions.Extensions, extensionPermissions) { return fmt.Errorf("incorrect Permissions.Extensions: Expected: %v, Actual: %v", extensionPermissions, cert.Permissions.Extensions) } if !reflect.DeepEqual(cert.Permissions.CriticalOptions, criticalOptionPermissions) { return fmt.Errorf("incorrect Permissions.CriticalOptions: %v", cert.Permissions.CriticalOptions) } return nil } func getSigningPublicKey() (ssh.PublicKey, error) { key, err := base64.StdEncoding.DecodeString(strings.Split(testCAPublicKey, " ")[1]) if err != nil { return nil, err } parsedKey, err := ssh.ParsePublicKey(key) if err != nil { return nil, err } return parsedKey, nil } func testConfigZeroAddressDelete(t *testing.T) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.DeleteOperation, Path: "config/zeroaddress", } } func testConfigZeroAddressWrite(t *testing.T, data map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config/zeroaddress", Data: data, } } func testConfigZeroAddressRead(t *testing.T, expected map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, Path: "config/zeroaddress", Check: func(resp *logical.Response) error { var d zeroAddressRoles if err := mapstructure.Decode(resp.Data, &d); err != nil { return err } var ex zeroAddressRoles if err := mapstructure.Decode(expected, &ex); err != nil { return err } if !reflect.DeepEqual(d, ex) { return fmt.Errorf("Response mismatch:\nActual:%#v\nExpected:%#v", d, ex) } return nil }, } } func testVerifyWrite(t *testing.T, data map[string]interface{}, expected map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: fmt.Sprintf("verify"), Data: data, Check: func(resp *logical.Response) error { var ac api.SSHVerifyResponse if err := mapstructure.Decode(resp.Data, &ac); err != nil { return err } var ex api.SSHVerifyResponse if err := mapstructure.Decode(expected, &ex); err != nil { return err } if !reflect.DeepEqual(ac, ex) { return fmt.Errorf("invalid response") } return nil }, } } func testLookupRead(t *testing.T, data map[string]interface{}, expected []string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "lookup", Data: data, Check: func(resp *logical.Response) error { if resp.Data == nil || resp.Data["roles"] == nil { return fmt.Errorf("missing roles information") } if !reflect.DeepEqual(resp.Data["roles"].([]string), expected) { return fmt.Errorf("Invalid response: \nactual:%#v\nexpected:%#v", resp.Data["roles"].([]string), expected) } return nil }, } } func testRoleWrite(t *testing.T, name string, data map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "roles/" + name, Data: data, } } func testRoleList(t *testing.T, expected map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ListOperation, Path: "roles", Check: func(resp *logical.Response) error { if resp == nil { return fmt.Errorf("nil response") } if resp.Data == nil { return fmt.Errorf("nil data") } if !reflect.DeepEqual(resp.Data, expected) { return fmt.Errorf("Invalid response:\nactual:%#v\nexpected is %#v", resp.Data, expected) } return nil }, } } func testRoleRead(t *testing.T, roleName string, expected map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, Path: "roles/" + roleName, Check: func(resp *logical.Response) error { if resp == nil { if expected == nil { return nil } return fmt.Errorf("bad: %#v", resp) } var d sshRole if err := mapstructure.Decode(resp.Data, &d); err != nil { return fmt.Errorf("error decoding response:%s", err) } switch d.KeyType { case "otp": if d.KeyType != expected["key_type"] || d.DefaultUser != expected["default_user"] || d.CIDRList != expected["cidr_list"] { return fmt.Errorf("data mismatch. bad: %#v", resp) } default: return fmt.Errorf("unknown key type. bad: %#v", resp) } return nil }, } } func testRoleDelete(t *testing.T, name string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.DeleteOperation, Path: "roles/" + name, } } func testCredsWrite(t *testing.T, roleName string, data map[string]interface{}, expectError bool, address string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: fmt.Sprintf("creds/%s", roleName), Data: data, ErrorOk: expectError, Check: func(resp *logical.Response) error { if resp == nil { return fmt.Errorf("response is nil") } if resp.Data == nil { return fmt.Errorf("data is nil") } if expectError { var e struct { Error string `mapstructure:"error"` } if err := mapstructure.Decode(resp.Data, &e); err != nil { return err } if len(e.Error) == 0 { return fmt.Errorf("expected error, but write succeeded") } return nil } if roleName == testAtRoleName { var d struct { Key string `mapstructure:"key"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { return err } if d.Key == "" { return fmt.Errorf("generated key is an empty string") } // Checking only for a parsable key privKey, err := ssh.ParsePrivateKey([]byte(d.Key)) if err != nil { return fmt.Errorf("generated key is invalid") } if err := testSSH(data["username"].(string), address, ssh.PublicKeys(privKey), "date"); err != nil { return fmt.Errorf("unable to SSH with new key (%s): %w", d.Key, err) } } else { if resp.Data["key_type"] != KeyTypeOTP { return fmt.Errorf("incorrect key_type") } if resp.Data["key"] == nil { return fmt.Errorf("invalid key") } } return nil }, } }