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:
parent
597e97264e
commit
881ae5a303
|
@ -53,7 +53,6 @@ func Backend(conf *logical.BackendConfig) (*backend, error) {
|
||||||
|
|
||||||
Paths: []*framework.Path{
|
Paths: []*framework.Path{
|
||||||
pathConfigZeroAddress(&b),
|
pathConfigZeroAddress(&b),
|
||||||
pathKeys(&b),
|
|
||||||
pathListRoles(&b),
|
pathListRoles(&b),
|
||||||
pathRoles(&b),
|
pathRoles(&b),
|
||||||
pathCredsCreate(&b),
|
pathCredsCreate(&b),
|
||||||
|
@ -66,7 +65,6 @@ func Backend(conf *logical.BackendConfig) (*backend, error) {
|
||||||
},
|
},
|
||||||
|
|
||||||
Secrets: []*framework.Secret{
|
Secrets: []*framework.Secret{
|
||||||
secretDynamicKey(&b),
|
|
||||||
secretOTP(&b),
|
secretOTP(&b),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -112,8 +110,8 @@ const backendHelp = `
|
||||||
The SSH backend generates credentials allowing clients to establish SSH
|
The SSH backend generates credentials allowing clients to establish SSH
|
||||||
connections to remote hosts.
|
connections to remote hosts.
|
||||||
|
|
||||||
There are three variants of the backend, which generate different types of
|
There are two variants of the backend, which generate different types of
|
||||||
credentials: dynamic keys, One-Time Passwords (OTPs) and certificate authority. The desired behavior
|
credentials: One-Time Passwords (OTPs) and certificate authority. The desired behavior
|
||||||
is role-specific and chosen at role creation time with the 'key_type'
|
is role-specific and chosen at role creation time with the 'key_type'
|
||||||
parameter.
|
parameter.
|
||||||
|
|
||||||
|
|
|
@ -26,17 +26,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testIP = "127.0.0.1"
|
testIP = "127.0.0.1"
|
||||||
testUserName = "vaultssh"
|
testUserName = "vaultssh"
|
||||||
testMultiUserName = "vaultssh,otherssh"
|
testMultiUserName = "vaultssh,otherssh"
|
||||||
testAdminUser = "vaultssh"
|
testAdminUser = "vaultssh"
|
||||||
testCaKeyType = "ca"
|
testCaKeyType = "ca"
|
||||||
testOTPKeyType = "otp"
|
testOTPKeyType = "otp"
|
||||||
testDynamicKeyType = "dynamic"
|
testCIDRList = "127.0.0.1/32"
|
||||||
testCIDRList = "127.0.0.1/32"
|
testAtRoleName = "test@RoleName"
|
||||||
testAtRoleName = "test@RoleName"
|
testOTPRoleName = "testOTPRoleName"
|
||||||
testDynamicRoleName = "testDynamicRoleName"
|
|
||||||
testOTPRoleName = "testOTPRoleName"
|
|
||||||
// testKeyName is the name of the entry that will be written to SSHMOUNTPOINT/ssh/keys
|
// testKeyName is the name of the entry that will be written to SSHMOUNTPOINT/ssh/keys
|
||||||
testKeyName = "testKeyName"
|
testKeyName = "testKeyName"
|
||||||
// testSharedPrivateKey is the value of the entry that will be written to SSHMOUNTPOINT/ssh/keys
|
// 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,
|
"default_user": testUserName,
|
||||||
"cidr_list": testCIDRList,
|
"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{}{
|
data := map[string]interface{}{
|
||||||
"ip": testIP,
|
"ip": testIP,
|
||||||
}
|
}
|
||||||
resp1 := []string(nil)
|
resp1 := []string(nil)
|
||||||
resp2 := []string{testOTPRoleName}
|
resp2 := []string{testOTPRoleName}
|
||||||
resp3 := []string{testDynamicRoleName, testOTPRoleName}
|
resp3 := []string{testAtRoleName}
|
||||||
resp4 := []string{testDynamicRoleName}
|
|
||||||
resp5 := []string{testAtRoleName}
|
|
||||||
logicaltest.Test(t, logicaltest.TestCase{
|
logicaltest.Test(t, logicaltest.TestCase{
|
||||||
LogicalFactory: newTestingFactory(t),
|
LogicalFactory: newTestingFactory(t),
|
||||||
Steps: []logicaltest.TestStep{
|
Steps: []logicaltest.TestStep{
|
||||||
testLookupRead(t, data, resp1),
|
testLookupRead(t, data, resp1),
|
||||||
testRoleWrite(t, testOTPRoleName, testOTPRoleData),
|
testRoleWrite(t, testOTPRoleName, testOTPRoleData),
|
||||||
testLookupRead(t, data, resp2),
|
testLookupRead(t, data, resp2),
|
||||||
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
|
|
||||||
testRoleWrite(t, testDynamicRoleName, testDynamicRoleData),
|
|
||||||
testLookupRead(t, data, resp3),
|
|
||||||
testRoleDelete(t, testOTPRoleName),
|
testRoleDelete(t, testOTPRoleName),
|
||||||
testLookupRead(t, data, resp4),
|
|
||||||
testRoleDelete(t, testDynamicRoleName),
|
|
||||||
testLookupRead(t, data, resp1),
|
testLookupRead(t, data, resp1),
|
||||||
testRoleWrite(t, testAtRoleName, testDynamicRoleData),
|
testRoleWrite(t, testAtRoleName, testOTPRoleData),
|
||||||
testLookupRead(t, data, resp5),
|
testLookupRead(t, data, resp3),
|
||||||
testRoleDelete(t, testAtRoleName),
|
testRoleDelete(t, testAtRoleName),
|
||||||
testLookupRead(t, data, resp1),
|
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) {
|
func TestSSHBackend_OTPRoleCrud(t *testing.T) {
|
||||||
testOTPRoleData := map[string]interface{}{
|
testOTPRoleData := map[string]interface{}{
|
||||||
"key_type": testOTPKeyType,
|
"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) {
|
func TestSSHBackend_OTPCreate(t *testing.T) {
|
||||||
cleanup, sshAddress := prepareTestContainer(t, "", "")
|
cleanup, sshAddress := prepareTestContainer(t, "", "")
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -772,24 +679,14 @@ func TestSSHBackend_ConfigZeroAddressCRUD(t *testing.T) {
|
||||||
"default_user": testUserName,
|
"default_user": testUserName,
|
||||||
"cidr_list": testCIDRList,
|
"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{}{
|
req1 := map[string]interface{}{
|
||||||
"roles": testOTPRoleName,
|
"roles": testOTPRoleName,
|
||||||
}
|
}
|
||||||
resp1 := map[string]interface{}{
|
resp1 := map[string]interface{}{
|
||||||
"roles": []string{testOTPRoleName},
|
"roles": []string{testOTPRoleName},
|
||||||
}
|
}
|
||||||
req2 := map[string]interface{}{
|
|
||||||
"roles": fmt.Sprintf("%s,%s", testOTPRoleName, testDynamicRoleName),
|
|
||||||
}
|
|
||||||
resp2 := map[string]interface{}{
|
resp2 := map[string]interface{}{
|
||||||
"roles": []string{testOTPRoleName, testDynamicRoleName},
|
"roles": []string{testOTPRoleName},
|
||||||
}
|
}
|
||||||
resp3 := map[string]interface{}{
|
resp3 := map[string]interface{}{
|
||||||
"roles": []string{},
|
"roles": []string{},
|
||||||
|
@ -801,11 +698,7 @@ func TestSSHBackend_ConfigZeroAddressCRUD(t *testing.T) {
|
||||||
testRoleWrite(t, testOTPRoleName, testOTPRoleData),
|
testRoleWrite(t, testOTPRoleName, testOTPRoleData),
|
||||||
testConfigZeroAddressWrite(t, req1),
|
testConfigZeroAddressWrite(t, req1),
|
||||||
testConfigZeroAddressRead(t, resp1),
|
testConfigZeroAddressRead(t, resp1),
|
||||||
testNamedKeysWrite(t, testKeyName, testSharedPrivateKey),
|
|
||||||
testRoleWrite(t, testDynamicRoleName, testDynamicRoleData),
|
|
||||||
testConfigZeroAddressWrite(t, req2),
|
|
||||||
testConfigZeroAddressRead(t, resp2),
|
testConfigZeroAddressRead(t, resp2),
|
||||||
testRoleDelete(t, testDynamicRoleName),
|
|
||||||
testConfigZeroAddressRead(t, resp1),
|
testConfigZeroAddressRead(t, resp1),
|
||||||
testRoleDelete(t, testOTPRoleName),
|
testRoleDelete(t, testOTPRoleName),
|
||||||
testConfigZeroAddressRead(t, resp3),
|
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) {
|
func TestSSHBackend_CA(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
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 {
|
func testLookupRead(t *testing.T, data map[string]interface{}, expected []string) logicaltest.TestStep {
|
||||||
return logicaltest.TestStep{
|
return logicaltest.TestStep{
|
||||||
Operation: logical.UpdateOperation,
|
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"] {
|
if d.KeyType != expected["key_type"] || d.DefaultUser != expected["default_user"] || d.CIDRList != expected["cidr_list"] {
|
||||||
return fmt.Errorf("data mismatch. bad: %#v", resp)
|
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:
|
default:
|
||||||
return fmt.Errorf("unknown key type. bad: %#v", resp)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
if roleName == testDynamicRoleName || roleName == testAtRoleName {
|
if roleName == testAtRoleName {
|
||||||
var d struct {
|
var d struct {
|
||||||
Key string `mapstructure:"key"`
|
Key string `mapstructure:"key"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
`
|
|
||||||
)
|
|
|
@ -135,30 +135,7 @@ func (b *backend) pathCredsCreateWrite(ctx context.Context, req *logical.Request
|
||||||
"otp": otp,
|
"otp": otp,
|
||||||
})
|
})
|
||||||
} else if role.KeyType == KeyTypeDynamic {
|
} else if role.KeyType == KeyTypeDynamic {
|
||||||
// Generate an RSA key pair. This also installs the newly generated
|
return nil, fmt.Errorf("dynamic key types have been removed")
|
||||||
// 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,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("key type unknown")
|
return nil, fmt.Errorf("key type unknown")
|
||||||
}
|
}
|
||||||
|
@ -166,41 +143,6 @@ func (b *backend) pathCredsCreateWrite(ctx context.Context, req *logical.Request
|
||||||
return result, nil
|
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.
|
// 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) {
|
func (b *backend) GenerateSaltedOTP(ctx context.Context) (string, string, error) {
|
||||||
str, err := uuid.GenerateUUID()
|
str, err := uuid.GenerateUUID()
|
||||||
|
@ -319,12 +261,8 @@ Creates a credential for establishing SSH connection with the remote host.
|
||||||
|
|
||||||
const pathCredsCreateHelpDesc = `
|
const pathCredsCreateHelpDesc = `
|
||||||
This path will generate a new key for establishing SSH session with
|
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
|
target host. The key can be a One Time Password (OTP) using 'key_type'
|
||||||
Time Password (OTP), using 'key_type' parameter being 'dynamic' or
|
being 'otp'.
|
||||||
'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.
|
|
||||||
|
|
||||||
Keys will have a lease associated with them. The access keys can be
|
Keys will have a lease associated with them. The access keys can be
|
||||||
revoked by using the lease ID.
|
revoked by using the lease ID.
|
||||||
|
|
|
@ -16,7 +16,7 @@ func pathFetchPublicKey(b *backend) *framework.Path {
|
||||||
},
|
},
|
||||||
|
|
||||||
HelpSynopsis: `Retrieve the public key.`,
|
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.`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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/".
|
|
||||||
`
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
const (
|
const (
|
||||||
// KeyTypeOTP is an key of type OTP
|
// KeyTypeOTP is an key of type OTP
|
||||||
KeyTypeOTP = "otp"
|
KeyTypeOTP = "otp"
|
||||||
// KeyTypeDynamic is dynamic key type
|
// KeyTypeDynamic is dynamic key type; removed.
|
||||||
KeyTypeDynamic = "dynamic"
|
KeyTypeDynamic = "dynamic"
|
||||||
// KeyTypeCA is an key of type CA
|
// KeyTypeCA is an key of type CA
|
||||||
KeyTypeCA = "ca"
|
KeyTypeCA = "ca"
|
||||||
|
@ -32,24 +32,19 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Structure that represents a role in SSH backend. This is a common role structure
|
// 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.
|
// Some are applicable for one and not for other. It doesn't matter.
|
||||||
type sshRole struct {
|
type sshRole struct {
|
||||||
KeyType string `mapstructure:"key_type" json:"key_type"`
|
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"`
|
DefaultUser string `mapstructure:"default_user" json:"default_user"`
|
||||||
DefaultUserTemplate bool `mapstructure:"default_user_template" json:"default_user_template"`
|
DefaultUserTemplate bool `mapstructure:"default_user_template" json:"default_user_template"`
|
||||||
CIDRList string `mapstructure:"cidr_list" json:"cidr_list"`
|
CIDRList string `mapstructure:"cidr_list" json:"cidr_list"`
|
||||||
ExcludeCIDRList string `mapstructure:"exclude_cidr_list" json:"exclude_cidr_list"`
|
ExcludeCIDRList string `mapstructure:"exclude_cidr_list" json:"exclude_cidr_list"`
|
||||||
Port int `mapstructure:"port" json:"port"`
|
Port int `mapstructure:"port" json:"port"`
|
||||||
InstallScript string `mapstructure:"install_script" json:"install_script"`
|
|
||||||
AllowedUsers string `mapstructure:"allowed_users" json:"allowed_users"`
|
AllowedUsers string `mapstructure:"allowed_users" json:"allowed_users"`
|
||||||
AllowedUsersTemplate bool `mapstructure:"allowed_users_template" json:"allowed_users_template"`
|
AllowedUsersTemplate bool `mapstructure:"allowed_users_template" json:"allowed_users_template"`
|
||||||
AllowedDomains string `mapstructure:"allowed_domains" json:"allowed_domains"`
|
AllowedDomains string `mapstructure:"allowed_domains" json:"allowed_domains"`
|
||||||
AllowedDomainsTemplate bool `mapstructure:"allowed_domains_template" json:"allowed_domains_template"`
|
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"`
|
MaxTTL string `mapstructure:"max_ttl" json:"max_ttl"`
|
||||||
TTL string `mapstructure:"ttl" json:"ttl"`
|
TTL string `mapstructure:"ttl" json:"ttl"`
|
||||||
DefaultCriticalOptions map[string]string `mapstructure:"default_critical_options" json:"default_critical_options"`
|
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]
|
[Required for all types]
|
||||||
Name of the role being created.`,
|
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": {
|
"default_user": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: `
|
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.
|
Default username for which a credential will be generated.
|
||||||
When the endpoint 'creds/' is used without a username, this
|
When the endpoint 'creds/' is used without a username, this
|
||||||
value will be used as default username.`,
|
value will be used as default username.`,
|
||||||
|
@ -127,7 +102,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"default_user_template": {
|
"default_user_template": {
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `
|
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.
|
If set, Default user can be specified using identity template policies.
|
||||||
Non-templated users are also permitted.
|
Non-templated users are also permitted.
|
||||||
`,
|
`,
|
||||||
|
@ -136,7 +111,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"cidr_list": {
|
"cidr_list": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: `
|
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.
|
Comma separated list of CIDR blocks for which the role is applicable for.
|
||||||
CIDR blocks can belong to more than one role.`,
|
CIDR blocks can belong to more than one role.`,
|
||||||
DisplayAttrs: &framework.DisplayAttributes{
|
DisplayAttrs: &framework.DisplayAttributes{
|
||||||
|
@ -146,7 +121,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"exclude_cidr_list": {
|
"exclude_cidr_list": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: `
|
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
|
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
|
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.`,
|
by the role and certain parts of it needs to be kept out.`,
|
||||||
|
@ -157,7 +132,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"port": {
|
"port": {
|
||||||
Type: framework.TypeInt,
|
Type: framework.TypeInt,
|
||||||
Description: `
|
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
|
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
|
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
|
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,
|
Type: framework.TypeString,
|
||||||
Description: `
|
Description: `
|
||||||
[Required for all types]
|
[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.`,
|
'otp' type requires agent to be installed in remote hosts.`,
|
||||||
AllowedValues: []interface{}{"otp", "dynamic", "ca"},
|
AllowedValues: []interface{}{"otp", "ca"},
|
||||||
DisplayAttrs: &framework.DisplayAttributes{
|
DisplayAttrs: &framework.DisplayAttributes{
|
||||||
Value: "ca",
|
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": {
|
"allowed_users": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: `
|
Description: `
|
||||||
|
@ -210,7 +171,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"allowed_users_template": {
|
"allowed_users_template": {
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `
|
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.
|
If set, Allowed users can be specified using identity template policies.
|
||||||
Non-templated users are also permitted.
|
Non-templated users are also permitted.
|
||||||
`,
|
`,
|
||||||
|
@ -219,7 +180,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"allowed_domains": {
|
"allowed_domains": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: `
|
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
|
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.
|
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": {
|
"allowed_domains_template": {
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `
|
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.
|
If set, Allowed domains can be specified using identity template policies.
|
||||||
Non-templated domains are also permitted.
|
Non-templated domains are also permitted.
|
||||||
`,
|
`,
|
||||||
Default: false,
|
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": {
|
"ttl": {
|
||||||
Type: framework.TypeDurationSecond,
|
Type: framework.TypeDurationSecond,
|
||||||
Description: `
|
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
|
The lease duration if no specific lease duration is
|
||||||
requested. The lease duration controls the expiration
|
requested. The lease duration controls the expiration
|
||||||
of certificates issued by this backend. Defaults to
|
of certificates issued by this backend. Defaults to
|
||||||
|
@ -257,7 +209,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"max_ttl": {
|
"max_ttl": {
|
||||||
Type: framework.TypeDurationSecond,
|
Type: framework.TypeDurationSecond,
|
||||||
Description: `
|
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
|
The maximum allowed lease duration
|
||||||
`,
|
`,
|
||||||
DisplayAttrs: &framework.DisplayAttributes{
|
DisplayAttrs: &framework.DisplayAttributes{
|
||||||
|
@ -267,7 +219,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"allowed_critical_options": {
|
"allowed_critical_options": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: `
|
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.
|
A comma-separated list of critical options that certificates can have when signed.
|
||||||
To allow any critical options, set this to an empty string.
|
To allow any critical options, set this to an empty string.
|
||||||
`,
|
`,
|
||||||
|
@ -275,7 +227,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"allowed_extensions": {
|
"allowed_extensions": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: `
|
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.
|
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
|
An empty list means that no extension overrides are allowed by an end-user; explicitly
|
||||||
specify '*' to allow any extensions to be set.
|
specify '*' to allow any extensions to be set.
|
||||||
|
@ -284,8 +236,8 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"default_critical_options": {
|
"default_critical_options": {
|
||||||
Type: framework.TypeMap,
|
Type: framework.TypeMap,
|
||||||
Description: `
|
Description: `
|
||||||
[Not applicable for Dynamic type] [Not applicable for OTP type]
|
[Not applicable for OTP type] [Optional for CA type]
|
||||||
[Optional for CA type] Critical options certificates should
|
Critical options certificates should
|
||||||
have if none are provided when signing. This field takes in key
|
have if none are provided when signing. This field takes in key
|
||||||
value pairs in JSON format. Note that these are not restricted
|
value pairs in JSON format. Note that these are not restricted
|
||||||
by "allowed_critical_options". Defaults to none.
|
by "allowed_critical_options". Defaults to none.
|
||||||
|
@ -294,8 +246,8 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"default_extensions": {
|
"default_extensions": {
|
||||||
Type: framework.TypeMap,
|
Type: framework.TypeMap,
|
||||||
Description: `
|
Description: `
|
||||||
[Not applicable for Dynamic type] [Not applicable for OTP type]
|
[Not applicable for OTP type] [Optional for CA type]
|
||||||
[Optional for CA type] Extensions certificates should have if
|
Extensions certificates should have if
|
||||||
none are provided when signing. This field takes in key value
|
none are provided when signing. This field takes in key value
|
||||||
pairs in JSON format. Note that these are not restricted by
|
pairs in JSON format. Note that these are not restricted by
|
||||||
"allowed_extensions". Defaults to none.
|
"allowed_extensions". Defaults to none.
|
||||||
|
@ -304,7 +256,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"default_extensions_template": {
|
"default_extensions_template": {
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `
|
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.
|
If set, Default extension values can be specified using identity template policies.
|
||||||
Non-templated extension values are also permitted.
|
Non-templated extension values are also permitted.
|
||||||
`,
|
`,
|
||||||
|
@ -313,7 +265,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"allow_user_certificates": {
|
"allow_user_certificates": {
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `
|
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'.
|
If set, certificates are allowed to be signed for use as a 'user'.
|
||||||
`,
|
`,
|
||||||
Default: false,
|
Default: false,
|
||||||
|
@ -321,7 +273,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"allow_host_certificates": {
|
"allow_host_certificates": {
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `
|
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'.
|
If set, certificates are allowed to be signed for use as a 'host'.
|
||||||
`,
|
`,
|
||||||
Default: false,
|
Default: false,
|
||||||
|
@ -329,7 +281,7 @@ func pathRoles(b *backend) *framework.Path {
|
||||||
"allow_bare_domains": {
|
"allow_bare_domains": {
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `
|
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
|
If set, host certificates that are requested are allowed to use the base domains listed in
|
||||||
"allowed_domains", e.g. "example.com".
|
"allowed_domains", e.g. "example.com".
|
||||||
This is a separate option as in some cases this can be considered a security threat.
|
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": {
|
"allow_subdomains": {
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `
|
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".
|
If set, host certificates that are requested are allowed to use subdomains of those listed in "allowed_domains".
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
"allow_user_key_ids": {
|
"allow_user_key_ids": {
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `
|
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.
|
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.
|
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.
|
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": {
|
"key_id_format": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: `
|
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.
|
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 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.
|
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": {
|
"allowed_user_key_lengths": {
|
||||||
Type: framework.TypeMap,
|
Type: framework.TypeMap,
|
||||||
Description: `
|
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, allows the enforcement of key types and minimum key sizes to be signed.
|
If set, allows the enforcement of key types and minimum key sizes to be signed.
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
"algorithm_signer": {
|
"algorithm_signer": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
Description: `
|
Description: `
|
||||||
|
[Not applicable for OTP type] [Optional for CA type]
|
||||||
When supplied, this value specifies a signing algorithm for the key. Possible values:
|
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.
|
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,
|
Type: framework.TypeDurationSecond,
|
||||||
Default: 30,
|
Default: 30,
|
||||||
Description: `
|
Description: `
|
||||||
|
[Not applicable for OTP type] [Optional for CA type]
|
||||||
The duration that the SSH certificate should be backdated by at issuance.`,
|
The duration that the SSH certificate should be backdated by at issuance.`,
|
||||||
DisplayAttrs: &framework.DisplayAttributes{
|
DisplayAttrs: &framework.DisplayAttributes{
|
||||||
Name: "Not before duration",
|
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
|
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)
|
allowedUsers := d.Get("allowed_users").(string)
|
||||||
|
|
||||||
// Validate the CIDR blocks
|
// 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
|
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.
|
// Below are the only fields used from the role structure for OTP type.
|
||||||
roleEntry = sshRole{
|
roleEntry = sshRole{
|
||||||
DefaultUser: defaultUser,
|
DefaultUser: defaultUser,
|
||||||
|
@ -477,59 +424,7 @@ func (b *backend) pathRoleWrite(ctx context.Context, req *logical.Request, d *fr
|
||||||
Version: roleEntryVersion,
|
Version: roleEntryVersion,
|
||||||
}
|
}
|
||||||
} else if keyType == KeyTypeDynamic {
|
} else if keyType == KeyTypeDynamic {
|
||||||
defaultUser := d.Get("default_user").(string)
|
return logical.ErrorResponse("dynamic key type roles are no longer supported"), nil
|
||||||
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,
|
|
||||||
}
|
|
||||||
} else if keyType == KeyTypeCA {
|
} else if keyType == KeyTypeCA {
|
||||||
algorithmSigner := DefaultAlgorithmSigner
|
algorithmSigner := DefaultAlgorithmSigner
|
||||||
algorithmSignerRaw, ok := d.GetOk("algorithm_signer")
|
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,
|
"allow_user_key_ids": role.AllowUserKeyIDs,
|
||||||
"key_id_format": role.KeyIDFormat,
|
"key_id_format": role.KeyIDFormat,
|
||||||
"key_type": role.KeyType,
|
"key_type": role.KeyType,
|
||||||
"key_bits": role.KeyBits,
|
|
||||||
"default_critical_options": role.DefaultCriticalOptions,
|
"default_critical_options": role.DefaultCriticalOptions,
|
||||||
"default_extensions": role.DefaultExtensions,
|
"default_extensions": role.DefaultExtensions,
|
||||||
"default_extensions_template": role.DefaultExtensionsTemplate,
|
"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()),
|
"not_before_duration": int64(role.NotBeforeDuration.Seconds()),
|
||||||
}
|
}
|
||||||
case KeyTypeDynamic:
|
case KeyTypeDynamic:
|
||||||
result = map[string]interface{}{
|
return nil, fmt.Errorf("dynamic key type roles are no longer supported")
|
||||||
"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,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid key type: %v", role.KeyType)
|
return nil, fmt.Errorf("invalid key type: %v", role.KeyType)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ssh
|
package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
@ -11,9 +10,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/hashicorp/go-hclog"
|
|
||||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
@ -40,69 +37,6 @@ func generateRSAKeys(keyBits int) (publicKeyRsa string, privateKeyRsa string, er
|
||||||
return
|
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
|
// Takes an IP address and role name and checks if the IP is part
|
||||||
// of CIDR blocks belonging to the role.
|
// of CIDR blocks belonging to the role.
|
||||||
func roleContainsIP(ctx context.Context, s logical.Storage, roleName string, ip string) (bool, error) {
|
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
|
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) {
|
func parsePublicSSHKey(key string) (ssh.PublicKey, error) {
|
||||||
keyParts := strings.Split(key, " ")
|
keyParts := strings.Split(key, " ")
|
||||||
if len(keyParts) > 1 {
|
if len(keyParts) > 1 {
|
||||||
|
|
|
@ -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.
|
||||||
|
```
|
|
@ -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
|
in Vault. Since it is possible to enable secrets engines at any location, please
|
||||||
update your API calls accordingly.
|
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
|
## Create Role
|
||||||
|
|
||||||
This endpoint creates or updates a named 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
|
- `name` `(string: <required>)` – Specifies the name of the role to create. This
|
||||||
is part of the request URL.
|
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
|
- `default_user` `(string: "")` – Specifies the default username for which a
|
||||||
credential will be generated. When the endpoint `creds/` is used without 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
|
username, this value will be used as default username. Its recommended to
|
||||||
create individual roles for each username to ensure absolute isolation between
|
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
|
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`.
|
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.
|
will be returned to the client by Vault along with the OTP.
|
||||||
|
|
||||||
- `key_type` `(string: <required>)` – Specifies the type of credentials
|
- `key_type` `(string: <required>)` – Specifies the type of credentials
|
||||||
generated by this role. This can be either `otp`, `dynamic` or `ca`.
|
generated by this role. This can be either `otp` 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.
|
|
||||||
|
|
||||||
- `allowed_users` `(string: "")` – If this option is not specified, or if it is
|
- `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
|
`*`, 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
|
specified using identity template policies. Non-templated domains are also
|
||||||
permitted.
|
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
|
- `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
|
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.
|
system default value or the value of `max_ttl`, whichever is shorter.
|
||||||
|
@ -304,19 +228,6 @@ $ curl \
|
||||||
|
|
||||||
### Sample Response
|
### 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:
|
For an OTP role:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -374,9 +285,6 @@ $ curl \
|
||||||
"key_info": {
|
"key_info": {
|
||||||
"dev": {
|
"dev": {
|
||||||
"key_type": "ca"
|
"key_type": "ca"
|
||||||
},
|
|
||||||
"prod": {
|
|
||||||
"key_type": "dynamic"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -527,31 +435,6 @@ $ curl \
|
||||||
|
|
||||||
### Sample Response
|
### 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:
|
For an OTP role:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
@ -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.
|
|
|
@ -3,9 +3,8 @@ layout: docs
|
||||||
page_title: SSH - Secrets Engines
|
page_title: SSH - Secrets Engines
|
||||||
description: |-
|
description: |-
|
||||||
The Vault SSH secrets engine provides secure authentication and authorization
|
The Vault SSH secrets engine provides secure authentication and authorization
|
||||||
for access to machines via the SSH protocol. There are multiple modes to the
|
for access to machines via the SSH protocol. There are two modes to the Vault
|
||||||
Vault SSH secrets engine including signed SSH certificates, dynamic SSH keys,
|
SSH secrets engine including signed SSH certificates and one-time passwords.
|
||||||
and one-time passwords.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# SSH Secrets Engine
|
# SSH Secrets Engine
|
||||||
|
@ -22,10 +21,14 @@ individually documented on its own page.
|
||||||
|
|
||||||
- [Signed SSH Certificates](/vault/docs/secrets/ssh/signed-ssh-certificates)
|
- [Signed SSH Certificates](/vault/docs/secrets/ssh/signed-ssh-certificates)
|
||||||
- [One-time SSH Passwords](/vault/docs/secrets/ssh/one-time-ssh-passwords)
|
- [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.
|
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
|
## API
|
||||||
|
|
||||||
The SSH secrets engine has a full HTTP API. Please see the
|
The SSH secrets engine has a full HTTP API. Please see the
|
||||||
|
|
|
@ -1215,10 +1215,6 @@
|
||||||
{
|
{
|
||||||
"title": "SSH OTP",
|
"title": "SSH OTP",
|
||||||
"path": "secrets/ssh/one-time-ssh-passwords"
|
"path": "secrets/ssh/one-time-ssh-passwords"
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Dynamic Key",
|
|
||||||
"path": "secrets/ssh/dynamic-ssh-keys"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue