Remove dynamic keys from SSH Secrets Engine (#18874)

* Remove dynamic keys from SSH Secrets Engine

This removes the functionality of Vault creating keys and adding them to
the authorized keys file on hosts.

This functionality has been deprecated since Vault version 0.7.2.

The preferred alternative is to use the SSH CA method, which also allows
key generation but places limits on TTL and doesn't require Vault reach
out to provision each key on the specified host, making it much more
secure.

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

* Remove dynamic ssh references from documentation

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

* Add changelog entry

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

* Remove dynamic key secret type entirely

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

* Clarify changelog language

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

* Add removal notice to the website

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

---------

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
Alexander Scheel 2023-01-31 16:02:22 -05:00 committed by GitHub
parent 597e97264e
commit 881ae5a303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 67 additions and 1439 deletions

View File

@ -53,7 +53,6 @@ func Backend(conf *logical.BackendConfig) (*backend, error) {
Paths: []*framework.Path{
pathConfigZeroAddress(&b),
pathKeys(&b),
pathListRoles(&b),
pathRoles(&b),
pathCredsCreate(&b),
@ -66,7 +65,6 @@ func Backend(conf *logical.BackendConfig) (*backend, error) {
},
Secrets: []*framework.Secret{
secretDynamicKey(&b),
secretOTP(&b),
},
@ -112,8 +110,8 @@ const backendHelp = `
The SSH backend generates credentials allowing clients to establish SSH
connections to remote hosts.
There are three variants of the backend, which generate different types of
credentials: dynamic keys, One-Time Passwords (OTPs) and certificate authority. The desired behavior
There are two variants of the backend, which generate different types of
credentials: One-Time Passwords (OTPs) and certificate authority. The desired behavior
is role-specific and chosen at role creation time with the 'key_type'
parameter.

View File

@ -26,17 +26,15 @@ import (
)
const (
testIP = "127.0.0.1"
testUserName = "vaultssh"
testMultiUserName = "vaultssh,otherssh"
testAdminUser = "vaultssh"
testCaKeyType = "ca"
testOTPKeyType = "otp"
testDynamicKeyType = "dynamic"
testCIDRList = "127.0.0.1/32"
testAtRoleName = "test@RoleName"
testDynamicRoleName = "testDynamicRoleName"
testOTPRoleName = "testOTPRoleName"
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
@ -537,36 +535,22 @@ func TestSSHBackend_Lookup(t *testing.T) {
"default_user": testUserName,
"cidr_list": testCIDRList,
}
testDynamicRoleData := map[string]interface{}{
"key_type": testDynamicKeyType,
"key": testKeyName,
"admin_user": testAdminUser,
"default_user": testAdminUser,
"cidr_list": testCIDRList,
}
data := map[string]interface{}{
"ip": testIP,
}
resp1 := []string(nil)
resp2 := []string{testOTPRoleName}
resp3 := []string{testDynamicRoleName, testOTPRoleName}
resp4 := []string{testDynamicRoleName}
resp5 := []string{testAtRoleName}
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),
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
testRoleWrite(t, testDynamicRoleName, testDynamicRoleData),
testLookupRead(t, data, resp3),
testRoleDelete(t, testOTPRoleName),
testLookupRead(t, data, resp4),
testRoleDelete(t, testDynamicRoleName),
testLookupRead(t, data, resp1),
testRoleWrite(t, testAtRoleName, testDynamicRoleData),
testLookupRead(t, data, resp5),
testRoleWrite(t, testAtRoleName, testOTPRoleData),
testLookupRead(t, data, resp3),
testRoleDelete(t, testAtRoleName),
testLookupRead(t, data, resp1),
},
@ -615,39 +599,6 @@ func TestSSHBackend_RoleList(t *testing.T) {
})
}
func TestSSHBackend_DynamicKeyCreate(t *testing.T) {
cleanup, sshAddress := prepareTestContainer(t, "", "")
defer cleanup()
host, port, err := net.SplitHostPort(sshAddress)
if err != nil {
t.Fatal(err)
}
testDynamicRoleData := map[string]interface{}{
"key_type": testDynamicKeyType,
"key": testKeyName,
"admin_user": testAdminUser,
"default_user": testAdminUser,
"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{
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
testRoleWrite(t, testDynamicRoleName, testDynamicRoleData),
testCredsWrite(t, testDynamicRoleName, data, false, sshAddress),
testRoleWrite(t, testAtRoleName, testDynamicRoleData),
testCredsWrite(t, testAtRoleName, data, false, sshAddress),
},
})
}
func TestSSHBackend_OTPRoleCrud(t *testing.T) {
testOTPRoleData := map[string]interface{}{
"key_type": testOTPKeyType,
@ -675,50 +626,6 @@ func TestSSHBackend_OTPRoleCrud(t *testing.T) {
})
}
func TestSSHBackend_DynamicRoleCrud(t *testing.T) {
testDynamicRoleData := map[string]interface{}{
"key_type": testDynamicKeyType,
"key": testKeyName,
"admin_user": testAdminUser,
"default_user": testAdminUser,
"cidr_list": testCIDRList,
}
respDynamicRoleData := map[string]interface{}{
"cidr_list": testCIDRList,
"port": 22,
"install_script": DefaultPublicKeyInstallScript,
"key_bits": 1024,
"key": testKeyName,
"admin_user": testUserName,
"default_user": testUserName,
"key_type": testDynamicKeyType,
}
logicaltest.Test(t, logicaltest.TestCase{
LogicalFactory: newTestingFactory(t),
Steps: []logicaltest.TestStep{
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
testRoleWrite(t, testDynamicRoleName, testDynamicRoleData),
testRoleRead(t, testDynamicRoleName, respDynamicRoleData),
testRoleDelete(t, testDynamicRoleName),
testRoleRead(t, testDynamicRoleName, nil),
testRoleWrite(t, testAtRoleName, testDynamicRoleData),
testRoleRead(t, testAtRoleName, respDynamicRoleData),
testRoleDelete(t, testAtRoleName),
testRoleRead(t, testAtRoleName, nil),
},
})
}
func TestSSHBackend_NamedKeysCrud(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{
LogicalFactory: newTestingFactory(t),
Steps: []logicaltest.TestStep{
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
testNamedKeysDelete(t),
},
})
}
func TestSSHBackend_OTPCreate(t *testing.T) {
cleanup, sshAddress := prepareTestContainer(t, "", "")
defer func() {
@ -772,24 +679,14 @@ func TestSSHBackend_ConfigZeroAddressCRUD(t *testing.T) {
"default_user": testUserName,
"cidr_list": testCIDRList,
}
testDynamicRoleData := map[string]interface{}{
"key_type": testDynamicKeyType,
"key": testKeyName,
"admin_user": testAdminUser,
"default_user": testAdminUser,
"cidr_list": testCIDRList,
}
req1 := map[string]interface{}{
"roles": testOTPRoleName,
}
resp1 := map[string]interface{}{
"roles": []string{testOTPRoleName},
}
req2 := map[string]interface{}{
"roles": fmt.Sprintf("%s,%s", testOTPRoleName, testDynamicRoleName),
}
resp2 := map[string]interface{}{
"roles": []string{testOTPRoleName, testDynamicRoleName},
"roles": []string{testOTPRoleName},
}
resp3 := map[string]interface{}{
"roles": []string{},
@ -801,11 +698,7 @@ func TestSSHBackend_ConfigZeroAddressCRUD(t *testing.T) {
testRoleWrite(t, testOTPRoleName, testOTPRoleData),
testConfigZeroAddressWrite(t, req1),
testConfigZeroAddressRead(t, resp1),
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
testRoleWrite(t, testDynamicRoleName, testDynamicRoleData),
testConfigZeroAddressWrite(t, req2),
testConfigZeroAddressRead(t, resp2),
testRoleDelete(t, testDynamicRoleName),
testConfigZeroAddressRead(t, resp1),
testRoleDelete(t, testOTPRoleName),
testConfigZeroAddressRead(t, resp3),
@ -839,43 +732,6 @@ func TestSSHBackend_CredsForZeroAddressRoles_otp(t *testing.T) {
})
}
func TestSSHBackend_CredsForZeroAddressRoles_dynamic(t *testing.T) {
cleanup, sshAddress := prepareTestContainer(t, "", "")
defer cleanup()
host, port, err := net.SplitHostPort(sshAddress)
if err != nil {
t.Fatal(err)
}
dynamicRoleData := map[string]interface{}{
"key_type": testDynamicKeyType,
"key": testKeyName,
"admin_user": testAdminUser,
"default_user": testAdminUser,
"port": port,
}
data := map[string]interface{}{
"username": testUserName,
"ip": host,
}
req2 := map[string]interface{}{
"roles": testDynamicRoleName,
}
logicaltest.Test(t, logicaltest.TestCase{
LogicalFactory: newTestingFactory(t),
Steps: []logicaltest.TestStep{
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
testRoleWrite(t, testDynamicRoleName, dynamicRoleData),
testCredsWrite(t, testDynamicRoleName, data, true, sshAddress),
testConfigZeroAddressWrite(t, req2),
testCredsWrite(t, testDynamicRoleName, data, false, sshAddress),
testConfigZeroAddressDelete(t),
testCredsWrite(t, testDynamicRoleName, data, true, sshAddress),
},
})
}
func TestSSHBackend_CA(t *testing.T) {
testCases := []struct {
name string
@ -2414,23 +2270,6 @@ func testVerifyWrite(t *testing.T, data map[string]interface{}, expected map[str
}
}
func testNamedKeysWrite(t *testing.T, name, key string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: fmt.Sprintf("keys/%s", name),
Data: map[string]interface{}{
"key": key,
},
}
}
func testNamedKeysDelete(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.DeleteOperation,
Path: fmt.Sprintf("keys/%s", testKeyName),
}
}
func testLookupRead(t *testing.T, data map[string]interface{}, expected []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
@ -2495,10 +2334,6 @@ func testRoleRead(t *testing.T, roleName string, expected map[string]interface{}
if d.KeyType != expected["key_type"] || d.DefaultUser != expected["default_user"] || d.CIDRList != expected["cidr_list"] {
return fmt.Errorf("data mismatch. bad: %#v", resp)
}
case "dynamic":
if d.AdminUser != expected["admin_user"] || d.CIDRList != expected["cidr_list"] || d.KeyName != expected["key"] || d.KeyType != expected["key_type"] {
return fmt.Errorf("data mismatch. bad: %#v", resp)
}
default:
return fmt.Errorf("unknown key type. bad: %#v", resp)
}
@ -2539,7 +2374,7 @@ func testCredsWrite(t *testing.T, roleName string, data map[string]interface{},
}
return nil
}
if roleName == testDynamicRoleName || roleName == testAtRoleName {
if roleName == testAtRoleName {
var d struct {
Key string `mapstructure:"key"`
}

View File

@ -1,350 +0,0 @@
package ssh
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"path/filepath"
log "github.com/hashicorp/go-hclog"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
type comm struct {
client *ssh.Client
config *SSHCommConfig
conn net.Conn
address string
}
// SSHCommConfig is the structure used to configure the SSH communicator.
type SSHCommConfig struct {
// The configuration of the Go SSH connection
SSHConfig *ssh.ClientConfig
// Connection returns a new connection. The current connection
// in use will be closed as part of the Close method, or in the
// case an error occurs.
Connection func() (net.Conn, error)
// Pty, if true, will request a pty from the remote end.
Pty bool
// DisableAgent, if true, will not forward the SSH agent.
DisableAgent bool
// Logger for output
Logger log.Logger
}
// Creates a new communicator implementation over SSH. This takes
// an already existing TCP connection and SSH configuration.
func SSHCommNew(address string, config *SSHCommConfig) (result *comm, err error) {
// Establish an initial connection and connect
result = &comm{
config: config,
address: address,
}
if err = result.reconnect(); err != nil {
result = nil
return
}
return
}
func (c *comm) Close() error {
var err error
if c.conn != nil {
err = c.conn.Close()
}
c.conn = nil
c.client = nil
return err
}
func (c *comm) Upload(path string, input io.Reader, fi *os.FileInfo) error {
// The target directory and file for talking the SCP protocol
target_dir := filepath.Dir(path)
target_file := filepath.Base(path)
// On windows, filepath.Dir uses backslash separators (ie. "\tmp").
// This does not work when the target host is unix. Switch to forward slash
// which works for unix and windows
target_dir = filepath.ToSlash(target_dir)
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
return scpUploadFile(target_file, input, w, stdoutR, fi)
}
return c.scpSession("scp -vt "+target_dir, scpFunc)
}
func (c *comm) NewSession() (session *ssh.Session, err error) {
if c.client == nil {
err = errors.New("client not available")
} else {
session, err = c.client.NewSession()
}
if err != nil {
c.config.Logger.Error("ssh session open error, attempting reconnect", "error", err)
if err := c.reconnect(); err != nil {
c.config.Logger.Error("reconnect attempt failed", "error", err)
return nil, err
}
return c.client.NewSession()
}
return session, nil
}
func (c *comm) reconnect() error {
// Close previous connection.
if c.conn != nil {
c.Close()
}
var err error
c.conn, err = c.config.Connection()
if err != nil {
// Explicitly set this to the REAL nil. Connection() can return
// a nil implementation of net.Conn which will make the
// "if c.conn == nil" check fail above. Read here for more information
// on this psychotic language feature:
//
// http://golang.org/doc/faq#nil_error
c.conn = nil
c.config.Logger.Error("reconnection error", "error", err)
return err
}
sshConn, sshChan, req, err := ssh.NewClientConn(c.conn, c.address, c.config.SSHConfig)
if err != nil {
c.config.Logger.Error("handshake error", "error", err)
c.Close()
return err
}
if sshConn != nil {
c.client = ssh.NewClient(sshConn, sshChan, req)
}
c.connectToAgent()
return nil
}
func (c *comm) connectToAgent() {
if c.client == nil {
return
}
if c.config.DisableAgent {
return
}
// open connection to the local agent
socketLocation := os.Getenv("SSH_AUTH_SOCK")
if socketLocation == "" {
return
}
agentConn, err := net.Dial("unix", socketLocation)
if err != nil {
c.config.Logger.Error("could not connect to local agent socket", "socket_path", socketLocation)
return
}
defer agentConn.Close()
// create agent and add in auth
forwardingAgent := agent.NewClient(agentConn)
if forwardingAgent == nil {
c.config.Logger.Error("could not create agent client")
return
}
// add callback for forwarding agent to SSH config
// XXX - might want to handle reconnects appending multiple callbacks
auth := ssh.PublicKeysCallback(forwardingAgent.Signers)
c.config.SSHConfig.Auth = append(c.config.SSHConfig.Auth, auth)
agent.ForwardToAgent(c.client, forwardingAgent)
// Setup a session to request agent forwarding
session, err := c.NewSession()
if err != nil {
return
}
defer session.Close()
err = agent.RequestAgentForwarding(session)
if err != nil {
c.config.Logger.Error("error requesting agent forwarding", "error", err)
return
}
return
}
func (c *comm) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error {
session, err := c.NewSession()
if err != nil {
return err
}
defer session.Close()
// Get a pipe to stdin so that we can send data down
stdinW, err := session.StdinPipe()
if err != nil {
return err
}
// We only want to close once, so we nil w after we close it,
// and only close in the defer if it hasn't been closed already.
defer func() {
if stdinW != nil {
stdinW.Close()
}
}()
// Get a pipe to stdout so that we can get responses back
stdoutPipe, err := session.StdoutPipe()
if err != nil {
return err
}
stdoutR := bufio.NewReader(stdoutPipe)
// Set stderr to a bytes buffer
stderr := new(bytes.Buffer)
session.Stderr = stderr
// Start the sink mode on the other side
if err := session.Start(scpCommand); err != nil {
return err
}
// Call our callback that executes in the context of SCP. We ignore
// EOF errors if they occur because it usually means that SCP prematurely
// ended on the other side.
if err := f(stdinW, stdoutR); err != nil && err != io.EOF {
return err
}
// Close the stdin, which sends an EOF, and then set w to nil so that
// our defer func doesn't close it again since that is unsafe with
// the Go SSH package.
stdinW.Close()
stdinW = nil
// Wait for the SCP connection to close, meaning it has consumed all
// our data and has completed. Or has errored.
err = session.Wait()
if err != nil {
if exitErr, ok := err.(*ssh.ExitError); ok {
// Otherwise, we have an ExitErorr, meaning we can just read
// the exit status
c.config.Logger.Error("got non-zero exit status", "exit_status", exitErr.ExitStatus())
// If we exited with status 127, it means SCP isn't available.
// Return a more descriptive error for that.
if exitErr.ExitStatus() == 127 {
return errors.New(
"SCP failed to start. This usually means that SCP is not\n" +
"properly installed on the remote system.")
}
}
return err
}
return nil
}
// checkSCPStatus checks that a prior command sent to SCP completed
// successfully. If it did not complete successfully, an error will
// be returned.
func checkSCPStatus(r *bufio.Reader) error {
code, err := r.ReadByte()
if err != nil {
return err
}
if code != 0 {
// Treat any non-zero (really 1 and 2) as fatal errors
message, _, err := r.ReadLine()
if err != nil {
return fmt.Errorf("error reading error message: %w", err)
}
return errors.New(string(message))
}
return nil
}
func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader, fi *os.FileInfo) error {
var mode os.FileMode
var size int64
if fi != nil && (*fi).Mode().IsRegular() {
mode = (*fi).Mode().Perm()
size = (*fi).Size()
} else {
// Create a temporary file where we can copy the contents of the src
// so that we can determine the length, since SCP is length-prefixed.
tf, err := ioutil.TempFile("", "vault-ssh-upload")
if err != nil {
return fmt.Errorf("error creating temporary file for upload: %w", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
mode = 0o644
if _, err := io.Copy(tf, src); err != nil {
return err
}
// Sync the file so that the contents are definitely on disk, then
// read the length of it.
if err := tf.Sync(); err != nil {
return fmt.Errorf("error creating temporary file for upload: %w", err)
}
// Seek the file to the beginning so we can re-read all of it
if _, err := tf.Seek(0, 0); err != nil {
return fmt.Errorf("error creating temporary file for upload: %w", err)
}
tfi, err := tf.Stat()
if err != nil {
return fmt.Errorf("error creating temporary file for upload: %w", err)
}
size = tfi.Size()
src = tf
}
// Start the protocol
perms := fmt.Sprintf("C%04o", mode)
fmt.Fprintln(w, perms, size, dst)
if err := checkSCPStatus(r); err != nil {
return err
}
if _, err := io.CopyN(w, src, size); err != nil {
return err
}
fmt.Fprint(w, "\x00")
if err := checkSCPStatus(r); err != nil {
return err
}
return nil
}

View File

@ -1,71 +0,0 @@
package ssh
const (
// This is a constant representing a script to install and uninstall public
// key in remote hosts.
DefaultPublicKeyInstallScript = `
#!/bin/bash
#
# This is a default script which installs or uninstalls an RSA public key to/from
# authorized_keys file in a typical linux machine.
#
# If the platform differs or if the binaries used in this script are not available
# in target machine, use the 'install_script' parameter with 'roles/' endpoint to
# register a custom script (applicable for Dynamic type only).
#
# Vault server runs this script on the target machine with the following params:
#
# $1:INSTALL_OPTION: "install" or "uninstall"
#
# $2:PUBLIC_KEY_FILE: File name containing public key to be installed. Vault server
# uses UUID as name to avoid collisions with public keys generated for other requests.
#
# $3:AUTH_KEYS_FILE: Absolute path of the authorized_keys file.
# Currently, vault uses /home/<username>/.ssh/authorized_keys as the path.
#
# [Note: This script will be run by Vault using the registered admin username.
# Notice that some commands below are run as 'sudo'. For graceful execution of
# this script there should not be any password prompts. So, disable password
# prompt for the admin username registered with Vault.
set -e
# Storing arguments into variables, to increase readability of the script.
INSTALL_OPTION=$1
PUBLIC_KEY_FILE=$2
AUTH_KEYS_FILE=$3
# Delete the public key file and the temporary file
function cleanup
{
rm -f "$PUBLIC_KEY_FILE" temp_$PUBLIC_KEY_FILE
}
# 'cleanup' will be called if the script ends or if any command fails.
trap cleanup EXIT
# Return if the option is anything other than 'install' or 'uninstall'.
if [ "$INSTALL_OPTION" != "install" ] && [ "$INSTALL_OPTION" != "uninstall" ]; then
exit 1
fi
# use locking to avoid parallel script execution
(
flock --timeout 10 200
# Create the .ssh directory and authorized_keys file if it does not exist
SSH_DIR=$(dirname $AUTH_KEYS_FILE)
sudo mkdir -p "$SSH_DIR"
sudo touch "$AUTH_KEYS_FILE"
# Remove the key from authorized_keys file if it is already present.
# This step is common for both install and uninstall. Note that grep's
# return code is ignored, thus if grep fails all keys will be removed
# rather than none and it fails secure
sudo grep -vFf "$PUBLIC_KEY_FILE" "$AUTH_KEYS_FILE" > temp_$PUBLIC_KEY_FILE || true
cat temp_$PUBLIC_KEY_FILE | sudo tee "$AUTH_KEYS_FILE"
# Append the new public key to authorized_keys file
if [ "$INSTALL_OPTION" == "install" ]; then
cat "$PUBLIC_KEY_FILE" | sudo tee --append "$AUTH_KEYS_FILE"
fi
) 200> ${AUTH_KEYS_FILE}.lock
`
)

View File

@ -135,30 +135,7 @@ func (b *backend) pathCredsCreateWrite(ctx context.Context, req *logical.Request
"otp": otp,
})
} else if role.KeyType == KeyTypeDynamic {
// Generate an RSA key pair. This also installs the newly generated
// public key in the remote host.
dynamicPublicKey, dynamicPrivateKey, err := b.GenerateDynamicCredential(ctx, req, role, username, ip)
if err != nil {
return nil, err
}
// Return the information relevant to user of dynamic type and save
// information required for later use in internal section of secret.
result = b.Secret(SecretDynamicKeyType).Response(map[string]interface{}{
"key": dynamicPrivateKey,
"key_type": role.KeyType,
"username": username,
"ip": ip,
"port": role.Port,
}, map[string]interface{}{
"admin_user": role.AdminUser,
"username": username,
"ip": ip,
"host_key_name": role.KeyName,
"dynamic_public_key": dynamicPublicKey,
"port": role.Port,
"install_script": role.InstallScript,
})
return nil, fmt.Errorf("dynamic key types have been removed")
} else {
return nil, fmt.Errorf("key type unknown")
}
@ -166,41 +143,6 @@ func (b *backend) pathCredsCreateWrite(ctx context.Context, req *logical.Request
return result, nil
}
// Generates a RSA key pair and installs it in the remote target
func (b *backend) GenerateDynamicCredential(ctx context.Context, req *logical.Request, role *sshRole, username, ip string) (string, string, error) {
// Fetch the host key to be used for dynamic key installation
keyEntry, err := req.Storage.Get(ctx, fmt.Sprintf("keys/%s", role.KeyName))
if err != nil {
return "", "", fmt.Errorf("key %q not found: %w", role.KeyName, err)
}
if keyEntry == nil {
return "", "", fmt.Errorf("key %q not found", role.KeyName)
}
var hostKey sshHostKey
if err := keyEntry.DecodeJSON(&hostKey); err != nil {
return "", "", fmt.Errorf("error reading the host key: %w", err)
}
// Generate a new RSA key pair with the given key length.
dynamicPublicKey, dynamicPrivateKey, err := generateRSAKeys(role.KeyBits)
if err != nil {
return "", "", fmt.Errorf("error generating key: %w", err)
}
if len(role.KeyOptionSpecs) != 0 {
dynamicPublicKey = fmt.Sprintf("%s %s", role.KeyOptionSpecs, dynamicPublicKey)
}
// Add the public key to authorized_keys file in target machine
err = b.installPublicKeyInTarget(ctx, role.AdminUser, username, ip, role.Port, hostKey.Key, dynamicPublicKey, role.InstallScript, true)
if err != nil {
return "", "", fmt.Errorf("failed to add public key to authorized_keys file in target: %w", err)
}
return dynamicPublicKey, dynamicPrivateKey, nil
}
// Generates a UUID OTP and its salted value based on the salt of the backend.
func (b *backend) GenerateSaltedOTP(ctx context.Context) (string, string, error) {
str, err := uuid.GenerateUUID()
@ -319,12 +261,8 @@ Creates a credential for establishing SSH connection with the remote host.
const pathCredsCreateHelpDesc = `
This path will generate a new key for establishing SSH session with
target host. The key can either be a long lived dynamic key or a One
Time Password (OTP), using 'key_type' parameter being 'dynamic' or
'otp' respectively. For dynamic keys, a named key should be supplied.
Create named key using the 'keys/' endpoint, and this represents the
shared SSH key of target host. If this backend is mounted at 'ssh',
then "ssh/creds/web" would generate a key for 'web' role.
target host. The key can be a One Time Password (OTP) using 'key_type'
being 'otp'.
Keys will have a lease associated with them. The access keys can be
revoked by using the lease ID.

View File

@ -16,7 +16,7 @@ func pathFetchPublicKey(b *backend) *framework.Path {
},
HelpSynopsis: `Retrieve the public key.`,
HelpDescription: `This allows the public key, that this backend has been configured with, to be fetched. This is a raw response endpoint without JSON encoding; use -format=raw or an external tool (e.g., curl) to fetch this value.`,
HelpDescription: `This allows the public key of the SSH CA certificate that this backend has been configured with to be fetched. This is a raw response endpoint without JSON encoding; use -format=raw or an external tool (e.g., curl) to fetch this value.`,
}
}

View File

@ -1,110 +0,0 @@
package ssh
import (
"context"
"fmt"
"golang.org/x/crypto/ssh"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
type sshHostKey struct {
Key string `json:"key"`
}
func pathKeys(b *backend) *framework.Path {
return &framework.Path{
Pattern: "keys/" + framework.GenericNameRegex("key_name"),
Fields: map[string]*framework.FieldSchema{
"key_name": {
Type: framework.TypeString,
Description: "[Required] Name of the key",
},
"key": {
Type: framework.TypeString,
Description: "[Required] SSH private key with super user privileges in host",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathKeysWrite,
logical.DeleteOperation: b.pathKeysDelete,
},
HelpSynopsis: pathKeysSyn,
HelpDescription: pathKeysDesc,
}
}
func (b *backend) getKey(ctx context.Context, s logical.Storage, n string) (*sshHostKey, error) {
entry, err := s.Get(ctx, "keys/"+n)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result sshHostKey
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathKeysDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
keyName := d.Get("key_name").(string)
keyPath := fmt.Sprintf("keys/%s", keyName)
err := req.Storage.Delete(ctx, keyPath)
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathKeysWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
keyName := d.Get("key_name").(string)
if keyName == "" {
return logical.ErrorResponse("Missing key_name"), nil
}
keyString := d.Get("key").(string)
// Check if the key provided is infact a private key
signer, err := ssh.ParsePrivateKey([]byte(keyString))
if err != nil || signer == nil {
return logical.ErrorResponse("Invalid key"), nil
}
if keyString == "" {
return logical.ErrorResponse("Missing key"), nil
}
keyPath := fmt.Sprintf("keys/%s", keyName)
// Store the key
entry, err := logical.StorageEntryJSON(keyPath, map[string]interface{}{
"key": keyString,
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
return nil, nil
}
const pathKeysSyn = `
Register a shared private key with Vault.
`
const pathKeysDesc = `
Vault uses this key to install and uninstall dynamic keys in remote hosts. This
key should have sudoer privileges in remote hosts. This enables installing keys
for unprivileged usernames.
If this backend is mounted as "ssh", then the endpoint for registering shared
key is "ssh/keys/<name>". The name given here can be associated with any number
of roles via the endpoint "ssh/roles/".
`

View File

@ -17,7 +17,7 @@ import (
const (
// KeyTypeOTP is an key of type OTP
KeyTypeOTP = "otp"
// KeyTypeDynamic is dynamic key type
// KeyTypeDynamic is dynamic key type; removed.
KeyTypeDynamic = "dynamic"
// KeyTypeCA is an key of type CA
KeyTypeCA = "ca"
@ -32,24 +32,19 @@ const (
)
// Structure that represents a role in SSH backend. This is a common role structure
// for both OTP and Dynamic roles. Not all the fields are mandatory for both type.
// for both OTP and CA roles. Not all the fields are mandatory for both type.
// Some are applicable for one and not for other. It doesn't matter.
type sshRole struct {
KeyType string `mapstructure:"key_type" json:"key_type"`
KeyName string `mapstructure:"key" json:"key"`
KeyBits int `mapstructure:"key_bits" json:"key_bits"`
AdminUser string `mapstructure:"admin_user" json:"admin_user"`
DefaultUser string `mapstructure:"default_user" json:"default_user"`
DefaultUserTemplate bool `mapstructure:"default_user_template" json:"default_user_template"`
CIDRList string `mapstructure:"cidr_list" json:"cidr_list"`
ExcludeCIDRList string `mapstructure:"exclude_cidr_list" json:"exclude_cidr_list"`
Port int `mapstructure:"port" json:"port"`
InstallScript string `mapstructure:"install_script" json:"install_script"`
AllowedUsers string `mapstructure:"allowed_users" json:"allowed_users"`
AllowedUsersTemplate bool `mapstructure:"allowed_users_template" json:"allowed_users_template"`
AllowedDomains string `mapstructure:"allowed_domains" json:"allowed_domains"`
AllowedDomainsTemplate bool `mapstructure:"allowed_domains_template" json:"allowed_domains_template"`
KeyOptionSpecs string `mapstructure:"key_option_specs" json:"key_option_specs"`
MaxTTL string `mapstructure:"max_ttl" json:"max_ttl"`
TTL string `mapstructure:"ttl" json:"ttl"`
DefaultCriticalOptions map[string]string `mapstructure:"default_critical_options" json:"default_critical_options"`
@ -93,30 +88,10 @@ func pathRoles(b *backend) *framework.Path {
[Required for all types]
Name of the role being created.`,
},
"key": {
Type: framework.TypeString,
Description: `
[Required for Dynamic type] [Not applicable for OTP type] [Not applicable for CA type]
Name of the registered key in Vault. Before creating the role, use the
'keys/' endpoint to create a named key.`,
},
"admin_user": {
Type: framework.TypeString,
Description: `
[Required for Dynamic type] [Not applicable for OTP type] [Not applicable for CA type]
Admin user at remote host. The shared key being registered should be
for this user and should have root privileges. Everytime a dynamic
credential is being generated for other users, Vault uses this admin
username to login to remote host and install the generated credential
for the other user.`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Admin Username",
},
},
"default_user": {
Type: framework.TypeString,
Description: `
[Required for Dynamic type] [Required for OTP type] [Optional for CA type]
[Required for OTP type] [Optional for CA type]
Default username for which a credential will be generated.
When the endpoint 'creds/' is used without a username, this
value will be used as default username.`,
@ -127,7 +102,7 @@ func pathRoles(b *backend) *framework.Path {
"default_user_template": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If set, Default user can be specified using identity template policies.
Non-templated users are also permitted.
`,
@ -136,7 +111,7 @@ func pathRoles(b *backend) *framework.Path {
"cidr_list": {
Type: framework.TypeString,
Description: `
[Optional for Dynamic type] [Optional for OTP type] [Not applicable for CA type]
[Optional for OTP type] [Not applicable for CA type]
Comma separated list of CIDR blocks for which the role is applicable for.
CIDR blocks can belong to more than one role.`,
DisplayAttrs: &framework.DisplayAttributes{
@ -146,7 +121,7 @@ func pathRoles(b *backend) *framework.Path {
"exclude_cidr_list": {
Type: framework.TypeString,
Description: `
[Optional for Dynamic type] [Optional for OTP type] [Not applicable for CA type]
[Optional for OTP type] [Not applicable for CA type]
Comma separated list of CIDR blocks. IP addresses belonging to these blocks are not
accepted by the role. This is particularly useful when big CIDR blocks are being used
by the role and certain parts of it needs to be kept out.`,
@ -157,7 +132,7 @@ func pathRoles(b *backend) *framework.Path {
"port": {
Type: framework.TypeInt,
Description: `
[Optional for Dynamic type] [Optional for OTP type] [Not applicable for CA type]
[Optional for OTP type] [Not applicable for CA type]
Port number for SSH connection. Default is '22'. Port number does not
play any role in creation of OTP. For 'otp' type, this is just a way
to inform client about the port number to use. Port number will be
@ -170,27 +145,13 @@ func pathRoles(b *backend) *framework.Path {
Type: framework.TypeString,
Description: `
[Required for all types]
Type of key used to login to hosts. It can be either 'otp', 'dynamic' or 'ca'.
Type of key used to login to hosts. It can be either 'otp' or 'ca'.
'otp' type requires agent to be installed in remote hosts.`,
AllowedValues: []interface{}{"otp", "dynamic", "ca"},
AllowedValues: []interface{}{"otp", "ca"},
DisplayAttrs: &framework.DisplayAttributes{
Value: "ca",
},
},
"key_bits": {
Type: framework.TypeInt,
Description: `
[Optional for Dynamic type] [Not applicable for OTP type] [Not applicable for CA type]
Length of the RSA dynamic key in bits. It is 1024 by default or it can be 2048.`,
},
"install_script": {
Type: framework.TypeString,
Description: `
[Optional for Dynamic type] [Not-applicable for OTP type] [Not applicable for CA type]
Script used to install and uninstall public keys in the target machine.
The inbuilt default install script will be for Linux hosts. For sample
script, refer the project documentation website.`,
},
"allowed_users": {
Type: framework.TypeString,
Description: `
@ -210,7 +171,7 @@ func pathRoles(b *backend) *framework.Path {
"allowed_users_template": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If set, Allowed users can be specified using identity template policies.
Non-templated users are also permitted.
`,
@ -219,7 +180,7 @@ func pathRoles(b *backend) *framework.Path {
"allowed_domains": {
Type: framework.TypeString,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If this option is not specified, client can request for a signed certificate for any
valid host. If only certain domains are allowed, then this list enforces it.
`,
@ -227,25 +188,16 @@ func pathRoles(b *backend) *framework.Path {
"allowed_domains_template": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If set, Allowed domains can be specified using identity template policies.
Non-templated domains are also permitted.
`,
Default: false,
},
"key_option_specs": {
Type: framework.TypeString,
Description: `
[Optional for Dynamic type] [Not applicable for OTP type] [Not applicable for CA type]
Comma separated option specifications which will be prefixed to RSA key in
authorized_keys file. Options should be valid and comply with authorized_keys
file format and should not contain spaces.
`,
},
"ttl": {
Type: framework.TypeDurationSecond,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
The lease duration if no specific lease duration is
requested. The lease duration controls the expiration
of certificates issued by this backend. Defaults to
@ -257,7 +209,7 @@ func pathRoles(b *backend) *framework.Path {
"max_ttl": {
Type: framework.TypeDurationSecond,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
The maximum allowed lease duration
`,
DisplayAttrs: &framework.DisplayAttributes{
@ -267,7 +219,7 @@ func pathRoles(b *backend) *framework.Path {
"allowed_critical_options": {
Type: framework.TypeString,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
A comma-separated list of critical options that certificates can have when signed.
To allow any critical options, set this to an empty string.
`,
@ -275,7 +227,7 @@ func pathRoles(b *backend) *framework.Path {
"allowed_extensions": {
Type: framework.TypeString,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
A comma-separated list of extensions that certificates can have when signed.
An empty list means that no extension overrides are allowed by an end-user; explicitly
specify '*' to allow any extensions to be set.
@ -284,8 +236,8 @@ func pathRoles(b *backend) *framework.Path {
"default_critical_options": {
Type: framework.TypeMap,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type]
[Optional for CA type] Critical options certificates should
[Not applicable for OTP type] [Optional for CA type]
Critical options certificates should
have if none are provided when signing. This field takes in key
value pairs in JSON format. Note that these are not restricted
by "allowed_critical_options". Defaults to none.
@ -294,8 +246,8 @@ func pathRoles(b *backend) *framework.Path {
"default_extensions": {
Type: framework.TypeMap,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type]
[Optional for CA type] Extensions certificates should have if
[Not applicable for OTP type] [Optional for CA type]
Extensions certificates should have if
none are provided when signing. This field takes in key value
pairs in JSON format. Note that these are not restricted by
"allowed_extensions". Defaults to none.
@ -304,7 +256,7 @@ func pathRoles(b *backend) *framework.Path {
"default_extensions_template": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If set, Default extension values can be specified using identity template policies.
Non-templated extension values are also permitted.
`,
@ -313,7 +265,7 @@ func pathRoles(b *backend) *framework.Path {
"allow_user_certificates": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If set, certificates are allowed to be signed for use as a 'user'.
`,
Default: false,
@ -321,7 +273,7 @@ func pathRoles(b *backend) *framework.Path {
"allow_host_certificates": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If set, certificates are allowed to be signed for use as a 'host'.
`,
Default: false,
@ -329,7 +281,7 @@ func pathRoles(b *backend) *framework.Path {
"allow_bare_domains": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If set, host certificates that are requested are allowed to use the base domains listed in
"allowed_domains", e.g. "example.com".
This is a separate option as in some cases this can be considered a security threat.
@ -338,14 +290,14 @@ func pathRoles(b *backend) *framework.Path {
"allow_subdomains": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If set, host certificates that are requested are allowed to use subdomains of those listed in "allowed_domains".
`,
},
"allow_user_key_ids": {
Type: framework.TypeBool,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
If true, users can override the key ID for a signed certificate with the "key_id" field.
When false, the key ID will always be the token display name.
The key ID is logged by the SSH server and can be useful for auditing.
@ -357,7 +309,7 @@ func pathRoles(b *backend) *framework.Path {
"key_id_format": {
Type: framework.TypeString,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
[Not applicable for OTP type] [Optional for CA type]
When supplied, this value specifies a custom format for the key id of a signed certificate.
The following variables are available for use: '{{token_display_name}}' - The display name of
the token used to make the request. '{{role_name}}' - The name of the role signing the request.
@ -370,13 +322,14 @@ func pathRoles(b *backend) *framework.Path {
"allowed_user_key_lengths": {
Type: framework.TypeMap,
Description: `
[Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type]
If set, allows the enforcement of key types and minimum key sizes to be signed.
`,
[Not applicable for OTP type] [Optional for CA type]
If set, allows the enforcement of key types and minimum key sizes to be signed.
`,
},
"algorithm_signer": {
Type: framework.TypeString,
Description: `
[Not applicable for OTP type] [Optional for CA type]
When supplied, this value specifies a signing algorithm for the key. Possible values:
ssh-rsa, rsa-sha2-256, rsa-sha2-512, default, or the empty string.
`,
@ -389,6 +342,7 @@ func pathRoles(b *backend) *framework.Path {
Type: framework.TypeDurationSecond,
Default: 30,
Description: `
[Not applicable for OTP type] [Optional for CA type]
The duration that the SSH certificate should be backdated by at issuance.`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Not before duration",
@ -414,7 +368,7 @@ func (b *backend) pathRoleWrite(ctx context.Context, req *logical.Request, d *fr
return logical.ErrorResponse("missing role name"), nil
}
// Allowed users is an optional field, applicable for both OTP and Dynamic types.
// Allowed users is an optional field, applicable for both OTP and CA types.
allowedUsers := d.Get("allowed_users").(string)
// Validate the CIDR blocks
@ -459,13 +413,6 @@ func (b *backend) pathRoleWrite(ctx context.Context, req *logical.Request, d *fr
return logical.ErrorResponse("missing default user"), nil
}
// Admin user is not used if OTP key type is used because there is
// no need to login to remote machine.
adminUser := d.Get("admin_user").(string)
if adminUser != "" {
return logical.ErrorResponse("admin user not required for OTP type"), nil
}
// Below are the only fields used from the role structure for OTP type.
roleEntry = sshRole{
DefaultUser: defaultUser,
@ -477,59 +424,7 @@ func (b *backend) pathRoleWrite(ctx context.Context, req *logical.Request, d *fr
Version: roleEntryVersion,
}
} else if keyType == KeyTypeDynamic {
defaultUser := d.Get("default_user").(string)
if defaultUser == "" {
return logical.ErrorResponse("missing default user"), nil
}
// Key name is required by dynamic type and not by OTP type.
keyName := d.Get("key").(string)
if keyName == "" {
return logical.ErrorResponse("missing key name"), nil
}
keyEntry, err := req.Storage.Get(ctx, fmt.Sprintf("keys/%s", keyName))
if err != nil || keyEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("invalid 'key': %q", keyName)), nil
}
installScript := d.Get("install_script").(string)
keyOptionSpecs := d.Get("key_option_specs").(string)
// Setting the default script here. The script will install the
// generated public key in the authorized_keys file of linux host.
if installScript == "" {
installScript = DefaultPublicKeyInstallScript
}
adminUser := d.Get("admin_user").(string)
if adminUser == "" {
return logical.ErrorResponse("missing admin username"), nil
}
// This defaults to 2048, but it can also be 1024, 3072, 4096, or 8192.
// In the near future, we should disallow 1024-bit SSH keys.
keyBits := d.Get("key_bits").(int)
if keyBits == 0 {
keyBits = 2048
}
if keyBits != 1024 && keyBits != 2048 && keyBits != 3072 && keyBits != 4096 && keyBits != 8192 {
return logical.ErrorResponse("invalid key_bits field"), nil
}
// Store all the fields required by dynamic key type
roleEntry = sshRole{
KeyName: keyName,
AdminUser: adminUser,
DefaultUser: defaultUser,
CIDRList: cidrList,
ExcludeCIDRList: excludeCidrList,
Port: port,
KeyType: KeyTypeDynamic,
KeyBits: keyBits,
InstallScript: installScript,
AllowedUsers: allowedUsers,
KeyOptionSpecs: keyOptionSpecs,
Version: roleEntryVersion,
}
return logical.ErrorResponse("dynamic key type roles are no longer supported"), nil
} else if keyType == KeyTypeCA {
algorithmSigner := DefaultAlgorithmSigner
algorithmSignerRaw, ok := d.GetOk("algorithm_signer")
@ -776,7 +671,6 @@ func (b *backend) parseRole(role *sshRole) (map[string]interface{}, error) {
"allow_user_key_ids": role.AllowUserKeyIDs,
"key_id_format": role.KeyIDFormat,
"key_type": role.KeyType,
"key_bits": role.KeyBits,
"default_critical_options": role.DefaultCriticalOptions,
"default_extensions": role.DefaultExtensions,
"default_extensions_template": role.DefaultExtensionsTemplate,
@ -785,23 +679,7 @@ func (b *backend) parseRole(role *sshRole) (map[string]interface{}, error) {
"not_before_duration": int64(role.NotBeforeDuration.Seconds()),
}
case KeyTypeDynamic:
result = map[string]interface{}{
"key": role.KeyName,
"admin_user": role.AdminUser,
"default_user": role.DefaultUser,
"cidr_list": role.CIDRList,
"exclude_cidr_list": role.ExcludeCIDRList,
"port": role.Port,
"key_type": role.KeyType,
"key_bits": role.KeyBits,
"allowed_users": role.AllowedUsers,
"key_option_specs": role.KeyOptionSpecs,
// Returning install script will make the output look messy.
// But this is one way for clients to see the script that is
// being used to install the key. If there is some problem,
// the script can be modified and configured by clients.
"install_script": role.InstallScript,
}
return nil, fmt.Errorf("dynamic key type roles are no longer supported")
default:
return nil, fmt.Errorf("invalid key type: %v", role.KeyType)
}

View File

@ -1,70 +0,0 @@
package ssh
import (
"context"
"fmt"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/mapstructure"
)
const SecretDynamicKeyType = "secret_dynamic_key_type"
func secretDynamicKey(b *backend) *framework.Secret {
return &framework.Secret{
Type: SecretDynamicKeyType,
Fields: map[string]*framework.FieldSchema{
"username": {
Type: framework.TypeString,
Description: "Username in host",
},
"ip": {
Type: framework.TypeString,
Description: "IP address of host",
},
},
Renew: b.secretDynamicKeyRenew,
Revoke: b.secretDynamicKeyRevoke,
}
}
func (b *backend) secretDynamicKeyRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return &logical.Response{Secret: req.Secret}, nil
}
func (b *backend) secretDynamicKeyRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
type sec struct {
AdminUser string `mapstructure:"admin_user"`
Username string `mapstructure:"username"`
IP string `mapstructure:"ip"`
HostKeyName string `mapstructure:"host_key_name"`
DynamicPublicKey string `mapstructure:"dynamic_public_key"`
InstallScript string `mapstructure:"install_script"`
Port int `mapstructure:"port"`
}
intSec := &sec{}
err := mapstructure.Decode(req.Secret.InternalData, intSec)
if err != nil {
return nil, fmt.Errorf("secret internal data could not be decoded: %w", err)
}
// Fetch the host key using the key name
hostKey, err := b.getKey(ctx, req.Storage, intSec.HostKeyName)
if err != nil {
return nil, fmt.Errorf("key %q not found error: %w", intSec.HostKeyName, err)
}
if hostKey == nil {
return nil, fmt.Errorf("key %q not found", intSec.HostKeyName)
}
// Remove the public key from authorized_keys file in target machine
// The last param 'false' indicates that the key should be uninstalled.
err = b.installPublicKeyInTarget(ctx, intSec.AdminUser, intSec.Username, intSec.IP, intSec.Port, hostKey.Key, intSec.DynamicPublicKey, intSec.InstallScript, false)
if err != nil {
return nil, fmt.Errorf("error removing public key from authorized_keys file in target")
}
return nil, nil
}

View File

@ -1,7 +1,6 @@
package ssh
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
@ -11,9 +10,7 @@ import (
"fmt"
"net"
"strings"
"time"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/vault/sdk/logical"
"golang.org/x/crypto/ssh"
@ -40,69 +37,6 @@ func generateRSAKeys(keyBits int) (publicKeyRsa string, privateKeyRsa string, er
return
}
// Public key and the script to install the key are uploaded to remote machine.
// Public key is either added or removed from authorized_keys file using the
// script. Default script is for a Linux machine and hence the path of the
// authorized_keys file is hard coded to resemble Linux.
//
// The last param 'install' if false, uninstalls the key.
func (b *backend) installPublicKeyInTarget(ctx context.Context, adminUser, username, ip string, port int, hostkey, dynamicPublicKey, installScript string, install bool) error {
// Transfer the newly generated public key to remote host under a random
// file name. This is to avoid name collisions from other requests.
_, publicKeyFileName, err := b.GenerateSaltedOTP(ctx)
if err != nil {
return err
}
comm, err := createSSHComm(b.Logger(), adminUser, ip, port, hostkey)
if err != nil {
return err
}
defer comm.Close()
err = comm.Upload(publicKeyFileName, bytes.NewBufferString(dynamicPublicKey), nil)
if err != nil {
return fmt.Errorf("error uploading public key: %w", err)
}
// Transfer the script required to install or uninstall the key to the remote
// host under a random file name as well. This is to avoid name collisions
// from other requests.
scriptFileName := fmt.Sprintf("%s.sh", publicKeyFileName)
err = comm.Upload(scriptFileName, bytes.NewBufferString(installScript), nil)
if err != nil {
return fmt.Errorf("error uploading install script: %w", err)
}
// Create a session to run remote command that triggers the script to install
// or uninstall the key.
session, err := comm.NewSession()
if err != nil {
return fmt.Errorf("unable to create SSH Session using public keys: %w", err)
}
if session == nil {
return fmt.Errorf("invalid session object")
}
defer session.Close()
authKeysFileName := fmt.Sprintf("/home/%s/.ssh/authorized_keys", username)
var installOption string
if install {
installOption = "install"
} else {
installOption = "uninstall"
}
// Give execute permissions to install script, run and delete it.
chmodCmd := fmt.Sprintf("chmod +x %s", scriptFileName)
scriptCmd := fmt.Sprintf("./%s %s %s %s", scriptFileName, installOption, publicKeyFileName, authKeysFileName)
rmCmd := fmt.Sprintf("rm -f %s", scriptFileName)
targetCmd := fmt.Sprintf("%s;%s;%s", chmodCmd, scriptCmd, rmCmd)
return session.Run(targetCmd)
}
// Takes an IP address and role name and checks if the IP is part
// of CIDR blocks belonging to the role.
func roleContainsIP(ctx context.Context, s logical.Storage, roleName string, ip string) (bool, error) {
@ -152,52 +86,6 @@ func cidrListContainsIP(ip, cidrList string) (bool, error) {
return false, nil
}
func insecureIgnoreHostWarning(logger log.Logger) ssh.HostKeyCallback {
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
logger.Warn("cannot verify server key: host key validation disabled")
return nil
}
}
func createSSHComm(logger log.Logger, username, ip string, port int, hostkey string) (*comm, error) {
signer, err := ssh.ParsePrivateKey([]byte(hostkey))
if err != nil {
return nil, err
}
clientConfig := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: insecureIgnoreHostWarning(logger),
Timeout: 1 * time.Minute,
}
connfunc := func() (net.Conn, error) {
c, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), 15*time.Second)
if err != nil {
return nil, err
}
if tcpConn, ok := c.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(5 * time.Second)
}
return c, nil
}
config := &SSHCommConfig{
SSHConfig: clientConfig,
Connection: connfunc,
Pty: false,
DisableAgent: true,
Logger: logger,
}
return SSHCommNew(fmt.Sprintf("%s:%d", ip, port), config)
}
func parsePublicSSHKey(key string) (ssh.PublicKey, error) {
keyParts := strings.Split(key, " ")
if len(keyParts) > 1 {

3
changelog/18874.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:security
secrets/ssh: removal of the deprecated dynamic keys mode. **When any remaining dynamic key leases expire**, an error stating `secret is unsupported by this backend` will be thrown by the lease manager.
```

View File

@ -14,62 +14,6 @@ This documentation assumes the SSH secrets engine is enabled at the `/ssh` path
in Vault. Since it is possible to enable secrets engines at any location, please
update your API calls accordingly.
## Create/Update Key
This endpoint creates or updates a named key.
| Method | Path |
| :----- | :---------------- |
| `POST` | `/ssh/keys/:name` |
### Parameters
- `name` `(string: <required>)`  Specifies the name of the key to create. This
is part of the request URL.
- `key` `(string: <required>)` Specifies an SSH private key with appropriate
privileges on remote hosts.
### Sample Payload
```json
{
"key": "..."
}
```
### Sample Request
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
--request POST \
--data @payload.json \
http://127.0.0.1:8200/v1/ssh/keys/my-key
```
## Delete Key
This endpoint deletes a named key.
| Method | Path |
| :------- | :---------------- |
| `DELETE` | `/ssh/keys/:name` |
### Parameters
- `name` `(string: <required>)`  Specifies the name of the key to delete. This
is part of the request URL.
### Sample Request
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
--request DELETE \
http://127.0.0.1:8200/v1/ssh/keys/my-key
```
## Create Role
This endpoint creates or updates a named role.
@ -83,21 +27,11 @@ This endpoint creates or updates a named role.
- `name` `(string: <required>)`  Specifies the name of the role to create. This
is part of the request URL.
- `key` `(string: "")`  Specifies the name of the registered key in Vault.
Before creating the role, use the `keys/` endpoint to create a named key. This
is required for "Dynamic Key" type.
- `admin_user` `(string: "")`  Specifies the admin user at remote host. The
shared key being registered should be for this user and should have root or
sudo privileges. Every time a dynamic credential is generated for a client,
Vault uses this admin username to login to remote host and install the
generated credential. This is required for Dynamic Key type.
- `default_user` `(string: "")`  Specifies the default username for which a
credential will be generated. When the endpoint `creds/` is used without a
username, this value will be used as default username. Its recommended to
create individual roles for each username to ensure absolute isolation between
usernames. This is required for Dynamic Key type and OTP type.
usernames. This is required for OTP type roles.
When `default_user_template` is set to `true`, this field can contain an identity
template with any prefix or suffix, like `ssh-{{identity.entity.id}}-user`.
@ -126,13 +60,7 @@ This endpoint creates or updates a named role.
will be returned to the client by Vault along with the OTP.
- `key_type` `(string: <required>)`  Specifies the type of credentials
generated by this role. This can be either `otp`, `dynamic` or `ca`.
- `key_bits` `(int: 1024)`  Specifies the length of the RSA dynamic key in
bits. This can be either 1024 or 2048.
- `install_script` `(string: "")`  Specifies the script used to install and
uninstall public keys in the target machine. Defaults to the built-in script.
generated by this role. This can be either `otp` or `ca`.
- `allowed_users` `(string: "")`  If this option is not specified, or if it is
`*`, the client can request a credential for any valid user at the remote
@ -158,10 +86,6 @@ This endpoint creates or updates a named role.
specified using identity template policies. Non-templated domains are also
permitted.
- `key_option_specs` `(string: "")`  Specifies a comma separated option
specification which will be prefixed to RSA keys in the remote host's
authorized_keys file. N.B.: Vault does not check this string for validity.
- `ttl` `(string: "")`  Specifies the Time To Live value provided as a string
duration with time suffix. Hour is the largest suffix. If not set, uses the
system default value or the value of `max_ttl`, whichever is shorter.
@ -304,19 +228,6 @@ $ curl \
### Sample Response
For a dynamic key role:
```json
{
"admin_user": "username",
"cidr_list": "x.x.x.x/y",
"default_user": "username",
"key": "<key name>",
"key_type": "dynamic",
"port": 22
}
```
For an OTP role:
```json
@ -374,9 +285,6 @@ $ curl \
"key_info": {
"dev": {
"key_type": "ca"
},
"prod": {
"key_type": "dynamic"
}
}
},
@ -527,31 +435,6 @@ $ curl \
### Sample Response
For a dynamic key role:
```json
{
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": {
"admin_user": "rajanadar",
"allowed_users": "",
"cidr_list": "x.x.x.x/y",
"default_user": "rajanadar",
"exclude_cidr_list": "x.x.x.x/y",
"install_script": "pretty_large_script",
"key": "5d9ee6a1-c787-47a9-9738-da243f4f69bf",
"key_bits": 1024,
"key_option_specs": "",
"key_type": "dynamic",
"port": 22
},
"warnings": null,
"auth": null
}
```
For an OTP role:
```json

View File

@ -1,193 +0,0 @@
---
layout: docs
page_title: Dynamic SSH Keys - SSH - Secrets Engines
description: |-
When using this type, the administrator registers a secret key with
appropriate sudo privileges on the remote machines. For every authorized
credential request, Vault creates a new SSH key pair and appends the
newly-generated public key to the authorized_keys file for the configured
username on the remote host. Vault uses a configurable install script to
achieve this.
---
# Dynamic SSH Keys
~> **Deprecated**: There are several serious drawbacks and security implications
inherent in this type. Because of these drawbacks, please use the SSH CA or OTP
types whenever possible.
When using this type, the administrator registers a secret key with appropriate
`sudo` privileges on the remote machines; for every authorized credential
request, Vault creates a new SSH key pair and appends the newly-generated public
key to the `authorized_keys` file for the configured username on the remote
host. Vault uses a configurable install script to achieve this.
The secrets engine does not prompt for `sudo` passwords; the `NOPASSWD` option for
sudoers should be enabled at all remote hosts for the Vault administrative
user.
The private key returned to the user will be leased and can be renewed if
desired. Once the key is given to the user, Vault will not know when it gets
used or how many time it gets used. Therefore, Vault **WILL NOT** and cannot
audit the SSH session establishments.
When the credential lease expires, Vault removes the secret key from the remote
machine.
This page will show a quick start for this secrets engine. For detailed documentation
on every path, use `vault path-help` after mounting the secrets engine.
### Drawbacks
The dynamic key type has several serious drawbacks:
1. _Audit logs are unreliable_: Vault can only log when users request
credentials, not when they use the given keys. If user A and user B both
request access to a machine, and are given a lease valid for five minutes,
it is impossible to know whether two accesses to that user account on the
remote machine were A, A; A, B; B, A; or B, B.
2. _Generating dynamic keys consumes entropy_: Unless equipped with a hardware
entropy generating device, a machine can quickly run out of entropy when
generating SSH keys. This will cause further requests for various Vault
operations to stall until more entropy is available, which could take a
significant amount of time, after which the next request for a new SSH key
will use the generated entropy and cause stalling again.
3. This type makes connections to client hosts; when this happens the host key
is _not_ verified.
### sudo
In order to adjust the `authorized_keys` file for the desired user, Vault
connects via SSH to the remote machine as a separate user, and uses `sudo` to
gain the privileges required. An example `sudoers` file is shown below.
File: `/etc/sudoers`
```hcl
# This is a sample sudoers statement; you should modify it
# as appropriate to satisfy your security needs.
vaultadmin ALL=(ALL)NOPASSWD: ALL
```
### Configuration
Next, infrastructure configuration must be registered with Vault via roles.
First, however, the shared secret key must be specified.
### Mount the secrets engine
```shell-session
$ vault secrets enable ssh
Successfully mounted 'ssh' at 'ssh'!
```
#### Registering the shared secret key
Register a key with a name; this key must have administrative capabilities on
the remote hosts.
```shell-session
$ vault write ssh/keys/dev_key \
key=@dev_shared_key.pem
```
#### Create a Role
Next, create a role. All of the machines contained within this CIDR block list
should be accessible using the registered shared secret key.
```shell-session
$ vault write ssh/roles/dynamic_key_role \
key_type=dynamic \
key=dev_key \
admin_user=username \
default_user=username \
cidr_list=x.x.x.x/y
Success! Data written to: ssh/roles/dynamic_key_role
```
`cidr_list` is a comma separated list of CIDR blocks for which a role can
generate credentials. If this is empty, the role can only generate credentials
if it belongs to the set of zero-address roles.
Zero-address roles, configured via `/ssh/config/zeroaddress` endpoint, takes
comma separated list of role names that can generate credentials for any IP
address.
Use the `install_script` option to provide an install script if the remote
hosts do not resemble a typical Linux machine. The default script is compiled
into the Vault binary, but it is straight forward to specify an alternate. The
script takes three arguments which are explained in the comments.
To see the default, see
[linux_install_script.go](https://github.com/hashicorp/vault/blob/main/builtin/logical/ssh/linux_install_script.go)
### Create a credential
Create a dynamic key for an IP of the remote host that is covered by
`dynamic_key_role`'s CIDR list.
```shell-session
$ vault write ssh/creds/dynamic_key_role ip=x.x.x.x
Key Value
lease_id ssh/creds/dynamic_key_role/8c4d2042-23bc-d6a8-42c2-6ff01cb83cf8
lease_duration 600
lease_renewable true
ip x.x.x.x
key -----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA5V/Y95qfGaUXRPkKNK9jgDHXPD2n5Ein+QTNnLSGrHtJUH7+
pgs/5Hc4//124P9qHNmjIYQVyvcLreFgSrQCq4K8193hmypBYtsvCgvpc+jEwaGA
zK0QV7uc1z8KL7FuRAxpHJwB6+nubOzzqM03xsViHRhaWhYVHw2Vl4oputSHE7R9
ugaTRg67wge4Nyi5RRL0RQcmW15/Vop8B6HpBSmZQy3enjg+32KbOWCMMTAPuF9/
DgxSgZQaFMjGN4RjDreZI8Vv5zIiFJzZ3KVOWy8piI0PblLnDpU4Q0QSQ9A+Vr7b
JS22Lbet1Zbapl/n947/r1wGObLCc5Lilu//1QIDAQABAoIBAHWLfdO9sETjHp6h
BULkkpgScpuTeSN6vGHXvUrOFKn1cCfJPNR4tWBuXI6LJM2+9nEccwXs+4IMwjZ0
ZfVCdI/SKtZxBXmP2PxBGMUMP7G/mn0kN64sDlD3ezOvQZgZVEmZFpCrvixYsG+v
qlpZ+HhrlJEWds7tvBsyyfNjwWjVIpm08zBmteFj4zu7OEcmGXEHDoxDXxyVP2BG
eLU/fM5JA2UEjfCQ1MIZ3rBtPePdz4LRpb+ajklqrUj1OHoiDrXa8EAf0/wDP9re
c1iH4bn7ZjYK0+IhZ+Pmw6gUftzZNWSC2kOLnZLdN/K7hgh0l0r0K/1eeXt43upB
WALNuiECgYEA8PM2Ob3XXKALF86PUewne4fCz9iixr/cIpvrEGrh9lyQRO8X5Jxb
ug38jEql4a574C6TSXfzxURza4P6lnfa0LvymmW0bhxZ5nev9kcAVnLKvpOUArTR
32k9bKXd6zp8Q9ZyVNwHRxcVs4YgwfJlcx8geC4o6YRiIjvcBQ9RVHkCgYEA87OK
lZDFBeEY/HVOxAQNXS5fgTd4U4DbwEJLv7SPk02v9oDkGHkpgMs4PcsIpCzsTpJ0
oXMfLSxZ1lmZiuUvAupKj/7RjJ0XyjSMfm1Zs81epWj+boVfM4amZNHVLIWgddmM
XzXEZKByvi1gs7qFcjQz2DEbZltWO6dX14O4Fz0CgYEAlWSWyHJWZ02r0xT1c7vS
NxtTxH7zXftzR9oYgtNiStfVc4gy7kGr9c3aOjnGZAlFMRhvpevDrxnj3lO0OTsS
5rzBjM1mc6cMboLjDPW01eTSpBroeE0Ym0arGQQ2djSK+5yowsixknhTsj2FbfsW
v6wa+6jTIQY9ujAXGOQIbzECgYAYuXlw7SwgCZNYYappFqQodQD5giAyEJu66L74
px/96N7WWoNJvFkqmPOOyV+KEIi0/ATbMGvUUHCY36RFRDU9zXldHJQz+Ogl+qja
VsvIAyj8DSfrHJrpBlsxVVyUVMZPzo+ARVs0flbF1qK9+Ul6qbMs1uaZvuCD0tmF
ovZ1XQKBgQDB0s7SDmAMgVjG8UBZgUru9vsDrxERT2BloptnnAjSiarLF5M+qeZO
7L4NLyVP39Z83eerEonzDAHHbvhPyi6n2YmnYhGjeP+lPZIVqGF9cpZD3q48YHZc
3ePn2/oLZrXKWOMyMwp2Uj+0SArCW+xMnoNp50sYNVR/JK3BPIdkag==
-----END RSA PRIVATE KEY-----
key_type dynamic
port 22
username username
```
### Establish an SSH session
Save the key to a file (e.g. `dyn_key.pem`) and then use it to establish an SSH
session.
```shell-session
$ ssh -i dyn_key.pem username@<IP of remote host>
username@<IP of remote host>:~$
```
### Automate it!
Creation of new key, saving to a file, and using it to establish an SSH session
can all be done with a single Vault CLI command.
```shell-session
$ vault ssh -role dynamic_key_role username@<IP of remote host>
username@<IP of remote host>:~$
```
## API
The SSH secret secrets engine has a full HTTP API. Please see the
[SSH secret secrets engine API](/vault/api-docs/secret/ssh) for more
details.

View File

@ -3,9 +3,8 @@ layout: docs
page_title: SSH - Secrets Engines
description: |-
The Vault SSH secrets engine provides secure authentication and authorization
for access to machines via the SSH protocol. There are multiple modes to the
Vault SSH secrets engine including signed SSH certificates, dynamic SSH keys,
and one-time passwords.
for access to machines via the SSH protocol. There are two modes to the Vault
SSH secrets engine including signed SSH certificates and one-time passwords.
---
# SSH Secrets Engine
@ -22,10 +21,14 @@ individually documented on its own page.
- [Signed SSH Certificates](/vault/docs/secrets/ssh/signed-ssh-certificates)
- [One-time SSH Passwords](/vault/docs/secrets/ssh/one-time-ssh-passwords)
- [Dynamic SSH Keys](/vault/docs/secrets/ssh/dynamic-ssh-keys) <sup>DEPRECATED</sup>
All guides assume a basic familiarity with the SSH protocol.
## Removal of Dynamic Keys feature
Per [Vault 1.12's deprecation notice page](/vault/docs/v1.12.x/deprecation),
the dynamic keys functionality of this engine has been removed in Vault 1.13.
## API
The SSH secrets engine has a full HTTP API. Please see the

View File

@ -1215,10 +1215,6 @@
{
"title": "SSH OTP",
"path": "secrets/ssh/one-time-ssh-passwords"
},
{
"title": "Dynamic Key",
"path": "secrets/ssh/dynamic-ssh-keys"
}
]
},