// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package ssh import ( "bytes" "context" "encoding/base64" "errors" "fmt" "net" "reflect" "strings" "testing" "time" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/testhelpers/corehelpers" "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" "github.com/stretchr/testify/require" ) 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" ) var ctx = context.Background() 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: corehelpers.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 }, } } func TestBackend_CleanupDynamicHostKeys(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) } // Running on a clean mount shouldn't do anything. cleanRequest := &logical.Request{ Operation: logical.DeleteOperation, Path: "tidy/dynamic-keys", Storage: config.StorageView, } resp, err := b.HandleRequest(context.Background(), cleanRequest) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, resp.Data) require.NotNil(t, resp.Data["message"]) require.Contains(t, resp.Data["message"], "0 of 0") // Write a bunch of bogus entries. for i := 0; i < 15; i++ { data := map[string]interface{}{ "host": "localhost", "key": "nothing-to-see-here", } entry, err := logical.StorageEntryJSON(fmt.Sprintf("%vexample-%v", keysStoragePrefix, i), &data) require.NoError(t, err) err = config.StorageView.Put(context.Background(), entry) require.NoError(t, err) } // Should now have 15 resp, err = b.HandleRequest(context.Background(), cleanRequest) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, resp.Data) require.NotNil(t, resp.Data["message"]) require.Contains(t, resp.Data["message"], "15 of 15") // Should have none left. resp, err = b.HandleRequest(context.Background(), cleanRequest) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, resp.Data) require.NotNil(t, resp.Data["message"]) require.Contains(t, resp.Data["message"], "0 of 0") } type pathAuthCheckerFunc func(t *testing.T, client *api.Client, path string, token string) func isPermDenied(err error) bool { return strings.Contains(err.Error(), "permission denied") } func isUnsupportedPathOperation(err error) bool { return strings.Contains(err.Error(), "unsupported path") || strings.Contains(err.Error(), "unsupported operation") } func isDeniedOp(err error) bool { return isPermDenied(err) || isUnsupportedPathOperation(err) } func pathShouldBeAuthed(t *testing.T, client *api.Client, path string, token string) { client.SetToken("") resp, err := client.Logical().ReadWithContext(ctx, path) if err == nil || !isPermDenied(err) { t.Fatalf("expected failure to read %v while unauthed: %v / %v", path, err, resp) } resp, err = client.Logical().ListWithContext(ctx, path) if err == nil || !isPermDenied(err) { t.Fatalf("expected failure to list %v while unauthed: %v / %v", path, err, resp) } resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) if err == nil || !isPermDenied(err) { t.Fatalf("expected failure to write %v while unauthed: %v / %v", path, err, resp) } resp, err = client.Logical().DeleteWithContext(ctx, path) if err == nil || !isPermDenied(err) { t.Fatalf("expected failure to delete %v while unauthed: %v / %v", path, err, resp) } resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) if err == nil || !isPermDenied(err) { t.Fatalf("expected failure to patch %v while unauthed: %v / %v", path, err, resp) } } func pathShouldBeUnauthedReadList(t *testing.T, client *api.Client, path string, token string) { // Should be able to read both with and without a token. client.SetToken("") resp, err := client.Logical().ReadWithContext(ctx, path) if err != nil && isPermDenied(err) { // Read will sometimes return permission denied, when the handler // does not support the given operation. Retry with the token. client.SetToken(token) resp2, err2 := client.Logical().ReadWithContext(ctx, path) if err2 != nil && !isUnsupportedPathOperation(err2) { t.Fatalf("unexpected failure to read %v while unauthed: %v / %v\nWhile authed: %v / %v", path, err, resp, err2, resp2) } client.SetToken("") } resp, err = client.Logical().ListWithContext(ctx, path) if err != nil && isPermDenied(err) { // List will sometimes return permission denied, when the handler // does not support the given operation. Retry with the token. client.SetToken(token) resp2, err2 := client.Logical().ListWithContext(ctx, path) if err2 != nil && !isUnsupportedPathOperation(err2) { t.Fatalf("unexpected failure to list %v while unauthed: %v / %v\nWhile authed: %v / %v", path, err, resp, err2, resp2) } client.SetToken("") } // These should all be denied. resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during write on read-only path %v while unauthed: %v / %v", path, err, resp) } resp, err = client.Logical().DeleteWithContext(ctx, path) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during delete on read-only path %v while unauthed: %v / %v", path, err, resp) } resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during patch on read-only path %v while unauthed: %v / %v", path, err, resp) } // Retrying with token should allow read/list, but not modification still. client.SetToken(token) resp, err = client.Logical().ReadWithContext(ctx, path) if err != nil && isPermDenied(err) { t.Fatalf("unexpected failure to read %v while authed: %v / %v", path, err, resp) } resp, err = client.Logical().ListWithContext(ctx, path) if err != nil && isPermDenied(err) { t.Fatalf("unexpected failure to list %v while authed: %v / %v", path, err, resp) } // Should all be denied. resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during write on read-only path %v while authed: %v / %v", path, err, resp) } resp, err = client.Logical().DeleteWithContext(ctx, path) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during delete on read-only path %v while authed: %v / %v", path, err, resp) } resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during patch on read-only path %v while authed: %v / %v", path, err, resp) } } func pathShouldBeUnauthedWriteOnly(t *testing.T, client *api.Client, path string, token string) { client.SetToken("") resp, err := client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) if err != nil && isPermDenied(err) { t.Fatalf("unexpected failure to write %v while unauthed: %v / %v", path, err, resp) } // These should all be denied. resp, err = client.Logical().ReadWithContext(ctx, path) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during read on write-only path %v while unauthed: %v / %v", path, err, resp) } resp, err = client.Logical().ListWithContext(ctx, path) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during list on write-only path %v while unauthed: %v / %v", path, err, resp) } resp, err = client.Logical().DeleteWithContext(ctx, path) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during delete on write-only path %v while unauthed: %v / %v", path, err, resp) } resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during patch on write-only path %v while unauthed: %v / %v", path, err, resp) } // Retrying with token should allow writing, but nothing else. client.SetToken(token) resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) if err != nil && isPermDenied(err) { t.Fatalf("unexpected failure to write %v while unauthed: %v / %v", path, err, resp) } // These should all be denied. resp, err = client.Logical().ReadWithContext(ctx, path) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during read on write-only path %v while authed: %v / %v", path, err, resp) } resp, err = client.Logical().ListWithContext(ctx, path) if err == nil || !isDeniedOp(err) { if resp != nil || err != nil { t.Fatalf("unexpected failure during list on write-only path %v while authed: %v / %v", path, err, resp) } } resp, err = client.Logical().DeleteWithContext(ctx, path) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during delete on write-only path %v while authed: %v / %v", path, err, resp) } resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) if err == nil || !isDeniedOp(err) { t.Fatalf("unexpected failure during patch on write-only path %v while authed: %v / %v", path, err, resp) } } type pathAuthChecker int const ( shouldBeAuthed pathAuthChecker = iota shouldBeUnauthedReadList shouldBeUnauthedWriteOnly ) var pathAuthChckerMap = map[pathAuthChecker]pathAuthCheckerFunc{ shouldBeAuthed: pathShouldBeAuthed, shouldBeUnauthedReadList: pathShouldBeUnauthedReadList, shouldBeUnauthedWriteOnly: pathShouldBeUnauthedWriteOnly, } func TestProperAuthing(t *testing.T) { t.Parallel() coreConfig := &vault.CoreConfig{ LogicalBackends: map[string]logical.Factory{ "ssh": Factory, }, } cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, }) cluster.Start() defer cluster.Cleanup() client := cluster.Cores[0].Client token := client.Token() // Mount SSH. err := client.Sys().MountWithContext(ctx, "ssh", &api.MountInput{ Type: "ssh", Config: api.MountConfigInput{ DefaultLeaseTTL: "16h", MaxLeaseTTL: "60h", }, }) if err != nil { t.Fatal(err) } // Setup basic configuration. _, err = client.Logical().WriteWithContext(ctx, "ssh/config/ca", map[string]interface{}{ "generate_signing_key": true, }) if err != nil { t.Fatal(err) } _, err = client.Logical().WriteWithContext(ctx, "ssh/roles/test-ca", map[string]interface{}{ "key_type": "ca", "allow_user_certificates": true, }) if err != nil { t.Fatal(err) } _, err = client.Logical().WriteWithContext(ctx, "ssh/issue/test-ca", map[string]interface{}{ "username": "toor", }) if err != nil { t.Fatal(err) } _, err = client.Logical().WriteWithContext(ctx, "ssh/roles/test-otp", map[string]interface{}{ "key_type": "otp", "default_user": "toor", "cidr_list": "127.0.0.0/24", }) if err != nil { t.Fatal(err) } resp, err := client.Logical().WriteWithContext(ctx, "ssh/creds/test-otp", map[string]interface{}{ "username": "toor", "ip": "127.0.0.1", }) if err != nil || resp == nil { t.Fatal(err) } // key := resp.Data["key"].(string) paths := map[string]pathAuthChecker{ "config/ca": shouldBeAuthed, "config/zeroaddress": shouldBeAuthed, "creds/test-otp": shouldBeAuthed, "issue/test-ca": shouldBeAuthed, "lookup": shouldBeAuthed, "public_key": shouldBeUnauthedReadList, "roles/test-ca": shouldBeAuthed, "roles/test-otp": shouldBeAuthed, "roles": shouldBeAuthed, "sign/test-ca": shouldBeAuthed, "tidy/dynamic-keys": shouldBeAuthed, "verify": shouldBeUnauthedWriteOnly, } for path, checkerType := range paths { checker := pathAuthChckerMap[checkerType] checker(t, client, "ssh/"+path, token) } client.SetToken(token) openAPIResp, err := client.Logical().ReadWithContext(ctx, "sys/internal/specs/openapi") if err != nil { t.Fatalf("failed to get openapi data: %v", err) } if len(openAPIResp.Data["paths"].(map[string]interface{})) == 0 { t.Fatalf("expected to get response from OpenAPI; got empty path list") } validatedPath := false for openapi_path, raw_data := range openAPIResp.Data["paths"].(map[string]interface{}) { if !strings.HasPrefix(openapi_path, "/ssh/") { t.Logf("Skipping path: %v", openapi_path) continue } t.Logf("Validating path: %v", openapi_path) validatedPath = true // Substitute values in from our testing map. raw_path := openapi_path[5:] if strings.Contains(raw_path, "{role}") && strings.Contains(raw_path, "roles/") { raw_path = strings.ReplaceAll(raw_path, "{role}", "test-ca") } if strings.Contains(raw_path, "{role}") && (strings.Contains(raw_path, "sign/") || strings.Contains(raw_path, "issue/")) { raw_path = strings.ReplaceAll(raw_path, "{role}", "test-ca") } if strings.Contains(raw_path, "{role}") && strings.Contains(raw_path, "creds") { raw_path = strings.ReplaceAll(raw_path, "{role}", "test-otp") } handler, present := paths[raw_path] if !present { t.Fatalf("OpenAPI reports SSH mount contains %v->%v but was not tested to be authed or authed.", openapi_path, raw_path) } openapi_data := raw_data.(map[string]interface{}) hasList := false rawGetData, hasGet := openapi_data["get"] if hasGet { getData := rawGetData.(map[string]interface{}) getParams, paramsPresent := getData["parameters"].(map[string]interface{}) if getParams != nil && paramsPresent { if _, hasList = getParams["list"]; hasList { // LIST is exclusive from GET on the same endpoint usually. hasGet = false } } } _, hasPost := openapi_data["post"] _, hasDelete := openapi_data["delete"] if handler == shouldBeUnauthedReadList { if hasPost || hasDelete { t.Fatalf("Unauthed read-only endpoints should not have POST/DELETE capabilities") } } } if !validatedPath { t.Fatalf("Expected to have validated at least one path.") } }