open-vault/builtin/logical/ssh/backend_test.go
Alexander Scheel 5d17f9b142
Allow cleanup ssh dynamic keys host keys (#18939)
* Add ability to clean up host keys for dynamic keys

This adds a new endpoint, tidy/dynamic-keys that removes any stale host
keys still present on the mount. This does not clean up any pending
dynamic key leases and will not remove these keys from systems with
authorized hosts entries created by Vault.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add documentation

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog entry

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

---------

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
2023-02-01 15:09:16 +00:00

2465 lines
79 KiB
Go

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"
)
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")
}