blob: 02224e0ef625581308da52593fcdf3899b1faae4 [file] [log] [blame] [edit]
// 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/builtin/credential/userpass"
"github.com/hashicorp/vault/helper/testhelpers/corehelpers"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/helper/docker"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)
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.")
}
}