Vault SSH: Documentation update and minor refactoring changes.
This commit is contained in:
parent
9db318fc55
commit
b91ebbc6e2
|
@ -2,8 +2,6 @@ package api
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
const SSHDefaultMountPoint = "ssh"
|
|
||||||
|
|
||||||
// SSH is used to return a client to invoke operations on SSH backend.
|
// SSH is used to return a client to invoke operations on SSH backend.
|
||||||
type SSH struct {
|
type SSH struct {
|
||||||
c *Client
|
c *Client
|
||||||
|
@ -12,7 +10,7 @@ type SSH struct {
|
||||||
|
|
||||||
// Returns the client for logical-backend API calls.
|
// Returns the client for logical-backend API calls.
|
||||||
func (c *Client) SSH() *SSH {
|
func (c *Client) SSH() *SSH {
|
||||||
return c.SSHWithMountPoint(SSHDefaultMountPoint)
|
return c.SSHWithMountPoint(SSHAgentDefaultMountPoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the client with specific SSH mount point.
|
// Returns the client with specific SSH mount point.
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Default path at which SSH backend will be mounted
|
// Default path at which SSH backend will be mounted in Vault server
|
||||||
SSHAgentDefaultMountPoint = "ssh"
|
SSHAgentDefaultMountPoint = "ssh"
|
||||||
|
|
||||||
// Echo request message sent as OTP by the agent
|
// Echo request message sent as OTP by the agent
|
||||||
|
@ -38,9 +38,15 @@ type SSHAgent struct {
|
||||||
// SSHVerifyResp is a structure representing the fields in Vault server's
|
// SSHVerifyResp is a structure representing the fields in Vault server's
|
||||||
// response.
|
// response.
|
||||||
type SSHVerifyResponse struct {
|
type SSHVerifyResponse struct {
|
||||||
Message string `mapstructure:"message"`
|
// Usually empty. If the request OTP is echo request message, this will
|
||||||
|
// be set to the corresponding echo response message.
|
||||||
|
Message string `mapstructure:"message"`
|
||||||
|
|
||||||
|
// Username associated with the OTP
|
||||||
Username string `mapstructure:"username"`
|
Username string `mapstructure:"username"`
|
||||||
IP string `mapstructure:"ip"`
|
|
||||||
|
// IP associated with the OTP
|
||||||
|
IP string `mapstructure:"ip"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Structure which represents the entries from the agent's configuration file.
|
// Structure which represents the entries from the agent's configuration file.
|
||||||
|
@ -53,7 +59,7 @@ type SSHAgentConfig struct {
|
||||||
AllowedCidrList string `hcl:"allowed_cidr_list"`
|
AllowedCidrList string `hcl:"allowed_cidr_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a HTTP client that uses TLS verification (TLS 1.2) with the given
|
// Returns a HTTP client that uses TLS verification (TLS 1.2) for a given
|
||||||
// certificate pool.
|
// certificate pool.
|
||||||
func (c *SSHAgentConfig) TLSClient(certPool *x509.CertPool) *http.Client {
|
func (c *SSHAgentConfig) TLSClient(certPool *x509.CertPool) *http.Client {
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
|
@ -113,7 +119,10 @@ func (c *SSHAgentConfig) NewClient() (*Client, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load agent's configuration from the file and populate the corresponding
|
// Load agent's configuration from the file and populate the corresponding
|
||||||
// in-memory structure. Vault address and SSH mount points required parameters.
|
// in-memory structure.
|
||||||
|
//
|
||||||
|
// Vault address is a required parameter.
|
||||||
|
// Mount point defaults to "ssh".
|
||||||
func LoadSSHAgentConfig(path string) (*SSHAgentConfig, error) {
|
func LoadSSHAgentConfig(path string) (*SSHAgentConfig, error) {
|
||||||
var config SSHAgentConfig
|
var config SSHAgentConfig
|
||||||
contents, err := ioutil.ReadFile(path)
|
contents, err := ioutil.ReadFile(path)
|
||||||
|
@ -134,7 +143,7 @@ func LoadSSHAgentConfig(path string) (*SSHAgentConfig, error) {
|
||||||
return nil, fmt.Errorf("config missing vault_addr")
|
return nil, fmt.Errorf("config missing vault_addr")
|
||||||
}
|
}
|
||||||
if config.SSHMountPoint == "" {
|
if config.SSHMountPoint == "" {
|
||||||
return nil, fmt.Errorf("config missing ssh_mount_point")
|
config.SSHMountPoint = SSHAgentDefaultMountPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
|
@ -155,9 +164,11 @@ func (c *Client) SSHAgentWithMountPoint(mountPoint string) *SSHAgent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifies if the key provided by user is present in Vault server. If yes,
|
// Verifies if the key provided by user is present in Vault server. The response
|
||||||
// the response will contain the IP address and username associated with the
|
// will contain the IP address and username associated with the OTP. In case the
|
||||||
// key.
|
// OTP matches the echo request message, instead of searching an entry for the OTP,
|
||||||
|
// an echo response message is returned. This feature is used by agent to verify if
|
||||||
|
// its configured correctly.
|
||||||
func (c *SSHAgent) Verify(otp string) (*SSHVerifyResponse, error) {
|
func (c *SSHAgent) Verify(otp string) (*SSHVerifyResponse, error) {
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"otp": otp,
|
"otp": otp,
|
||||||
|
|
|
@ -60,31 +60,30 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const backendHelp = `
|
const backendHelp = `
|
||||||
The SSH backend generates keys to eatablish SSH connection
|
The SSH backend generates credentials to establish SSH connection with remote hosts.
|
||||||
with remote hosts. There are two options to create the keys:
|
There are two types of credentials that could be generated: Dynamic and OTP. The
|
||||||
long lived dynamic key, one time password.
|
desired way of key creation should be chosen by using 'key_type' parameter of 'roles/'
|
||||||
|
endpoint. When a credential is requested for a particular role, Vault will generate
|
||||||
|
a credential accordingly and issue it.
|
||||||
|
|
||||||
Long lived dynamic key is a rsa private key which can be used
|
Dynamic Key: is a RSA private key which can be used to establish SSH session using
|
||||||
to login to remote host using the publickey authentication.
|
publickey authentication. When the client receives a key and uses it to establish
|
||||||
There is no additional change required in the remote hosts to
|
connections with hosts, Vault server will have no way to know when and how many
|
||||||
support this type of keys. But the keys generated will be valid
|
times the key will be used. So, these login attempts will not be audited by Vault.
|
||||||
as long as the lease of the key is valid. Also, logins to remote
|
To create a dynamic credential, Vault will use the shared private key registered
|
||||||
hosts will not be audited in vault server.
|
with the role. Named key should be created using 'keys/' endpoint and used with
|
||||||
|
'roles/' endpoint for Vault to know the shared key to use for installing the newly
|
||||||
|
generated key. Since Vault uses the shared key to install keys for other usernames,
|
||||||
|
shared key should have sudoer privileges in remote hosts and password prompts for
|
||||||
|
sudoers should be disabled. Also, dynamic keys are leased keys and gets revoked
|
||||||
|
in remote hosts by Vault after the expiry.
|
||||||
|
|
||||||
One Time Password (OTP), on the other hand is a randomly generated
|
OTP Key: is a UUID which can be used to login using keyboard-interactive authentication.
|
||||||
UUID that is used to login to remote host using the keyboard-
|
All the hosts that intend to support OTP should have Vault SSH Agent installed in
|
||||||
interactive challenge response authentication. A vault agent
|
them. This agent will receive the OTP from client and get it validated by Vault server.
|
||||||
has to be installed at the remote host to support OTP. Upon
|
And since Vault server has a role to play for each successful connection, all the
|
||||||
request, vault server generates and provides the key to the
|
events will be audited. Vault server validates a key only once, hence it is a OTP.
|
||||||
user. During login, vault agent receives the key and verifies
|
|
||||||
the correctness with the vault server (and hence audited). The
|
|
||||||
server after verifying the key for the first time, deletes the
|
|
||||||
same (and hence one-time).
|
|
||||||
|
|
||||||
Both type of keys have a configurable lease set and are automatically
|
After mounting this backend, before generating the keys, configure the lease using
|
||||||
revoked at the end of the lease.
|
'congig/lease' endpoint and create roles using 'roles/' endpoint.
|
||||||
|
|
||||||
After mounting this backend, before generating the keys, configure
|
|
||||||
the lease using the 'config/lease' endpoint and create roles using
|
|
||||||
the 'roles/' endpoint.
|
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package ssh
|
package ssh
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LinuxInstallScript = `
|
// This is a constant representing a script to install and uninstall public
|
||||||
|
// key in remote hosts.
|
||||||
|
DefaultPublicKeyInstallScript = `
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# This script file installs or uninstalls an RSA public key to/from authoried_keys
|
# This script file installs or uninstalls an RSA public key to/from authoried_keys
|
||||||
|
@ -16,27 +18,29 @@ const (
|
||||||
# as file name to avoid collisions with public keys generated for requests.
|
# as file name to avoid collisions with public keys generated for requests.
|
||||||
#
|
#
|
||||||
# $3: Absolute path of the authorized_keys file.
|
# $3: Absolute path of the authorized_keys file.
|
||||||
|
# Currently, vault uses /home/<username>/.ssh/authorized_keys as the path.
|
||||||
#
|
#
|
||||||
# [Note: Modify the script if targt machine does not have the commands used in
|
# [Note: This is a default script and is written to provide convenience.
|
||||||
# this script]
|
# If the host platform differs, or if the binaries used in this script are not
|
||||||
|
# available, write a new script that takes the above parameters and does the
|
||||||
|
# same task as this script, and register it Vault while role creation using
|
||||||
|
# 'install_script' parameter.
|
||||||
|
|
||||||
if [ $1 != "install" && $1 != "uninstall" ]; then
|
if [ $1 != "install" && $1 != "uninstall" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If the key being installed is already present in the authorized_keys file, it is
|
# Remove the key from authorized_key file if it is already present.
|
||||||
# removed and the result is stored in a temporary file.
|
# This step is common for both installing and uninstalling the key.
|
||||||
grep -vFf $2 $3 > temp_$2
|
grep -vFf $2 $3 > temp_$2
|
||||||
|
|
||||||
# Contents of temporary file will be the contents of authorized_keys file.
|
|
||||||
cat temp_$2 | sudo tee $3
|
cat temp_$2 | sudo tee $3
|
||||||
|
|
||||||
if [ $1 == "install" ]; then
|
if [ $1 == "install" ]; then
|
||||||
# New public key is appended to authorized_keys file
|
# Append the new public key to authorized_keys file
|
||||||
cat $2 | sudo tee --append $3
|
cat $2 | sudo tee --append $3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Auxiliary files are deleted
|
# Delete the auxiliary files
|
||||||
rm -f $2 temp_$2
|
rm -f $2 temp_$2
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
|
@ -99,10 +99,9 @@ Configure the default lease information for SSH dynamic keys.
|
||||||
`
|
`
|
||||||
|
|
||||||
const pathConfigLeaseHelpDesc = `
|
const pathConfigLeaseHelpDesc = `
|
||||||
This configures the default lease information used for SSH keys
|
This configures the default lease information used for SSH keys generated by
|
||||||
generated by this backend. The lease specifies the duration that a
|
this backend. The lease specifies the duration that a credential will be valid
|
||||||
credential will be valid for, as well as the maximum session for
|
for, as well as the maximum session for a set of credentials.
|
||||||
a set of credentials.
|
|
||||||
|
|
||||||
The format for the lease is "1h" or integer and then unit. The longest
|
The format for the lease is "1h" or integer and then unit. The longest
|
||||||
unit is hour.
|
unit is hour.
|
||||||
|
|
|
@ -41,18 +41,6 @@ func pathCredsCreate(b *backend) *framework.Path {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the username supplied by the user is present in the list of
|
|
||||||
// allowed users registered which creation of role.
|
|
||||||
func validateUsername(username, allowedUsers string) error {
|
|
||||||
userList := strings.Split(allowedUsers, ",")
|
|
||||||
for _, user := range userList {
|
|
||||||
if user == username {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("username not in allowed users list")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *backend) pathCredsCreateWrite(
|
func (b *backend) pathCredsCreateWrite(
|
||||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
roleName := d.Get("role").(string)
|
roleName := d.Get("role").(string)
|
||||||
|
@ -73,7 +61,9 @@ func (b *backend) pathCredsCreateWrite(
|
||||||
return logical.ErrorResponse(fmt.Sprintf("Role '%s' not found", roleName)), nil
|
return logical.ErrorResponse(fmt.Sprintf("Role '%s' not found", roleName)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// username is an optional parameter.
|
||||||
username := d.Get("username").(string)
|
username := d.Get("username").(string)
|
||||||
|
|
||||||
// Set the default username
|
// Set the default username
|
||||||
if username == "" {
|
if username == "" {
|
||||||
if role.DefaultUser == "" {
|
if role.DefaultUser == "" {
|
||||||
|
@ -112,10 +102,16 @@ func (b *backend) pathCredsCreateWrite(
|
||||||
|
|
||||||
var result *logical.Response
|
var result *logical.Response
|
||||||
if role.KeyType == KeyTypeOTP {
|
if role.KeyType == KeyTypeOTP {
|
||||||
|
// Generate an OTP
|
||||||
otp, err := b.GenerateOTPCredential(req, username, ip)
|
otp, err := b.GenerateOTPCredential(req, username, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the information relevant to user of OTP type and save
|
||||||
|
// the data required for later use in the internal section of secret.
|
||||||
|
// In this case, saving just the OTP is sufficient since there is
|
||||||
|
// no need to establish connection with the remote host.
|
||||||
result = b.Secret(SecretOTPType).Response(map[string]interface{}{
|
result = b.Secret(SecretOTPType).Response(map[string]interface{}{
|
||||||
"key_type": role.KeyType,
|
"key_type": role.KeyType,
|
||||||
"key": otp,
|
"key": otp,
|
||||||
|
@ -126,10 +122,15 @@ func (b *backend) pathCredsCreateWrite(
|
||||||
"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
|
||||||
|
// public key in the remote host.
|
||||||
dynamicPublicKey, dynamicPrivateKey, err := b.GenerateDynamicCredential(req, role, username, ip)
|
dynamicPublicKey, dynamicPrivateKey, err := b.GenerateDynamicCredential(req, role, username, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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{}{
|
result = b.Secret(SecretDynamicKeyType).Response(map[string]interface{}{
|
||||||
"key": dynamicPrivateKey,
|
"key": dynamicPrivateKey,
|
||||||
"key_type": role.KeyType,
|
"key_type": role.KeyType,
|
||||||
|
@ -152,11 +153,13 @@ func (b *backend) pathCredsCreateWrite(
|
||||||
// Change the lease information to reflect user's choice
|
// Change the lease information to reflect user's choice
|
||||||
lease, _ := b.Lease(req.Storage)
|
lease, _ := b.Lease(req.Storage)
|
||||||
|
|
||||||
|
// If the lease information is set, update it in secret.
|
||||||
if lease != nil {
|
if lease != nil {
|
||||||
result.Secret.Lease = lease.Lease
|
result.Secret.Lease = lease.Lease
|
||||||
result.Secret.LeaseGracePeriod = lease.LeaseMax
|
result.Secret.LeaseGracePeriod = lease.LeaseMax
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If lease information is not set, set it to 10 minutes.
|
||||||
if lease == nil {
|
if lease == nil {
|
||||||
result.Secret.Lease = 10 * time.Minute
|
result.Secret.Lease = 10 * time.Minute
|
||||||
result.Secret.LeaseGracePeriod = 2 * time.Minute
|
result.Secret.LeaseGracePeriod = 2 * time.Minute
|
||||||
|
@ -170,11 +173,11 @@ func (b *backend) GenerateDynamicCredential(req *logical.Request, role *sshRole,
|
||||||
// Fetch the host key to be used for dynamic key installation
|
// Fetch the host key to be used for dynamic key installation
|
||||||
keyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", role.KeyName))
|
keyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", role.KeyName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("key '%s' not found error:%s", role.KeyName, err)
|
return "", "", fmt.Errorf("key '%s' not found. err:%s", role.KeyName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyEntry == nil {
|
if keyEntry == nil {
|
||||||
return "", "", fmt.Errorf("key '%s' not found", role.KeyName, err)
|
return "", "", fmt.Errorf("key '%s' not found", role.KeyName)
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostKey sshHostKey
|
var hostKey sshHostKey
|
||||||
|
@ -182,26 +185,14 @@ func (b *backend) GenerateDynamicCredential(req *logical.Request, role *sshRole,
|
||||||
return "", "", fmt.Errorf("error reading the host key: %s", err)
|
return "", "", fmt.Errorf("error reading the host key: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a new RSA key pair with the given key length.
|
||||||
dynamicPublicKey, dynamicPrivateKey, err := generateRSAKeys(role.KeyBits)
|
dynamicPublicKey, dynamicPrivateKey, err := generateRSAKeys(role.KeyBits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("error generating key: %s", err)
|
return "", "", fmt.Errorf("error generating key: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer the public key to target machine
|
|
||||||
_, publicKeyFileName := b.GenerateSaltedOTP()
|
|
||||||
err = scpUpload(role.AdminUser, ip, role.Port, hostKey.Key, publicKeyFileName, dynamicPublicKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("error uploading public key: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptFileName := fmt.Sprintf("%s.sh", publicKeyFileName)
|
|
||||||
err = scpUpload(role.AdminUser, ip, role.Port, hostKey.Key, scriptFileName, role.InstallScript)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("error uploading install script: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the public key to authorized_keys file in target machine
|
// Add the public key to authorized_keys file in target machine
|
||||||
err = installPublicKeyInTarget(role.AdminUser, publicKeyFileName, username, ip, role.Port, hostKey.Key, true)
|
err = b.installPublicKeyInTarget(role.AdminUser, username, ip, role.Port, hostKey.Key, dynamicPublicKey, role.InstallScript, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("error adding public key to authorized_keys file in target")
|
return "", "", fmt.Errorf("error adding public key to authorized_keys file in target")
|
||||||
}
|
}
|
||||||
|
@ -217,8 +208,14 @@ func (b *backend) GenerateSaltedOTP() (string, string) {
|
||||||
// Generates an UUID OTP and creates an entry for the same in storage backend with its salted string.
|
// Generates an UUID OTP and creates an entry for the same in storage backend with its salted string.
|
||||||
func (b *backend) GenerateOTPCredential(req *logical.Request, username, ip string) (string, error) {
|
func (b *backend) GenerateOTPCredential(req *logical.Request, username, ip string) (string, error) {
|
||||||
otp, otpSalted := b.GenerateSaltedOTP()
|
otp, otpSalted := b.GenerateSaltedOTP()
|
||||||
|
|
||||||
|
// Check if there is an entry already created for the newly generated OTP.
|
||||||
entry, err := b.getOTP(req.Storage, otpSalted)
|
entry, err := b.getOTP(req.Storage, otpSalted)
|
||||||
// Make sure that new OTP is not replacing an existing one
|
|
||||||
|
// If entry already exists for the OTP, make sure that new OTP is not
|
||||||
|
// replacing an existing one by recreating new ones until an unused
|
||||||
|
// OTP is generated. It is very unlikely that this is the case and this
|
||||||
|
// code is just for safety.
|
||||||
for err == nil && entry != nil {
|
for err == nil && entry != nil {
|
||||||
otp, otpSalted = b.GenerateSaltedOTP()
|
otp, otpSalted = b.GenerateSaltedOTP()
|
||||||
entry, err = b.getOTP(req.Storage, otpSalted)
|
entry, err = b.getOTP(req.Storage, otpSalted)
|
||||||
|
@ -226,6 +223,8 @@ func (b *backend) GenerateOTPCredential(req *logical.Request, username, ip strin
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store an entry for the salt of OTP.
|
||||||
newEntry, err := logical.StorageEntryJSON("otp/"+otpSalted, sshOTP{
|
newEntry, err := logical.StorageEntryJSON("otp/"+otpSalted, sshOTP{
|
||||||
Username: username,
|
Username: username,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
|
@ -239,6 +238,18 @@ func (b *backend) GenerateOTPCredential(req *logical.Request, username, ip strin
|
||||||
return otp, nil
|
return otp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if the username supplied by the user is present in the list of
|
||||||
|
// allowed users registered which creation of role.
|
||||||
|
func validateUsername(username, allowedUsers string) error {
|
||||||
|
userList := strings.Split(allowedUsers, ",")
|
||||||
|
for _, user := range userList {
|
||||||
|
if user == username {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("username not in allowed users list")
|
||||||
|
}
|
||||||
|
|
||||||
const pathCredsCreateHelpSyn = `
|
const pathCredsCreateHelpSyn = `
|
||||||
Creates a credential for establishing SSH connection with the remote host.
|
Creates a credential for establishing SSH connection with the remote host.
|
||||||
`
|
`
|
||||||
|
|
|
@ -86,6 +86,7 @@ func (b *backend) pathKeysWrite(req *logical.Request, d *framework.FieldData) (*
|
||||||
|
|
||||||
keyString := d.Get("key").(string)
|
keyString := d.Get("key").(string)
|
||||||
|
|
||||||
|
// Check if the key provided is infact a private key
|
||||||
signer, err := ssh.ParsePrivateKey([]byte(keyString))
|
signer, err := ssh.ParsePrivateKey([]byte(keyString))
|
||||||
if err != nil || signer == nil {
|
if err != nil || signer == nil {
|
||||||
return logical.ErrorResponse("Invalid key"), nil
|
return logical.ErrorResponse("Invalid key"), nil
|
||||||
|
@ -97,6 +98,7 @@ func (b *backend) pathKeysWrite(req *logical.Request, d *framework.FieldData) (*
|
||||||
|
|
||||||
keyPath := fmt.Sprintf("keys/%s", keyName)
|
keyPath := fmt.Sprintf("keys/%s", keyName)
|
||||||
|
|
||||||
|
// Store the key
|
||||||
entry, err := logical.StorageEntryJSON(keyPath, map[string]interface{}{
|
entry, err := logical.StorageEntryJSON(keyPath, map[string]interface{}{
|
||||||
"key": keyString,
|
"key": keyString,
|
||||||
})
|
})
|
||||||
|
@ -110,18 +112,15 @@ func (b *backend) pathKeysWrite(req *logical.Request, d *framework.FieldData) (*
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathKeysSyn = `
|
const pathKeysSyn = `
|
||||||
Register a shared key which can be used to install dynamic key
|
Register a shared private key with Vault.
|
||||||
in remote machine.
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const pathKeysDesc = `
|
const pathKeysDesc = `
|
||||||
The shared key registered will be used to install and uninstall
|
Vault uses this key to install and uninstall dynamic keys in remote hosts. This
|
||||||
long lived dynamic keys in remote machine. This key should have
|
key should have sudoer privileges in remote hosts. This enables installing keys
|
||||||
"root" privileges at target machine. This enables installing keys
|
|
||||||
for unprivileged usernames.
|
for unprivileged usernames.
|
||||||
|
|
||||||
If this backend is mounted as "ssh", then the endpoint for registering
|
If this backend is mounted as "ssh", then the endpoint for registering shared key
|
||||||
shared key is "ssh/keys/webrack", if "webrack" is the user coined
|
is "ssh/keys/webrack", if "webrack" is the user coined name for the key. The name
|
||||||
name for the key. The name given here can be associated with any
|
given here can be associated with any number of roles via the endpoint "ssh/roles/".
|
||||||
number of roles via the endpoint "ssh/roles/".
|
|
||||||
`
|
`
|
||||||
|
|
|
@ -35,11 +35,14 @@ func (b *backend) pathLookupWrite(req *logical.Request, d *framework.FieldData)
|
||||||
return logical.ErrorResponse(fmt.Sprintf("Invalid IP '%s'", ip.String())), nil
|
return logical.ErrorResponse(fmt.Sprintf("Invalid IP '%s'", ip.String())), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all the roles created in the backend.
|
||||||
keys, err := req.Storage.List("roles/")
|
keys, err := req.Storage.List("roles/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look for roles which has CIDR blocks that encompasses the given IP
|
||||||
|
// and create a list out of it.
|
||||||
var matchingRoles []string
|
var matchingRoles []string
|
||||||
for _, role := range keys {
|
for _, role := range keys {
|
||||||
if contains, _ := roleContainsIP(req.Storage, role, ip.String()); contains {
|
if contains, _ := roleContainsIP(req.Storage, role, ip.String()); contains {
|
||||||
|
@ -47,13 +50,14 @@ func (b *backend) pathLookupWrite(req *logical.Request, d *framework.FieldData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This result may potentially reveal more information than it is supposed to.
|
// This list may potentially reveal more information than it is supposed to.
|
||||||
// The roles for which the client is not authorized to will also be displayed.
|
// The roles for which the client is not authorized to will also be displayed.
|
||||||
// However, if the client tries to use the role for which the client is not
|
// However, if the client tries to use the role for which the client is not
|
||||||
// authenticated, it will fail. There are no problems there. In a way this can
|
// authenticated, it will fail. It is not a problem. In a way this can be
|
||||||
// be viewed as a feature. The client can ask for permissions to be given for
|
// viewed as a feature. The client can ask for permissions to be given for
|
||||||
// a specific role if things are not working!
|
// a specific role if things are not working!
|
||||||
// Going forward, the role names should be filtered and only the roles which
|
//
|
||||||
|
// Ideally, the role names should be filtered and only the roles which
|
||||||
// the client is authorized to see, should be returned.
|
// the client is authorized to see, should be returned.
|
||||||
return &logical.Response{
|
return &logical.Response{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
|
@ -63,16 +67,15 @@ func (b *backend) pathLookupWrite(req *logical.Request, d *framework.FieldData)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathLookupSyn = `
|
const pathLookupSyn = `
|
||||||
Lists 'roles' that can be used to create a dynamic key.
|
List all the roles associated with the given IP address.
|
||||||
`
|
`
|
||||||
|
|
||||||
const pathLookupDesc = `
|
const pathLookupDesc = `
|
||||||
The IP address for which the key is requested, is searched in the
|
The IP address for which the key is requested, is searched in the CIDR blocks
|
||||||
CIDR blocks registered with vault using the 'roles' endpoint. Keys
|
registered with vault using the 'roles' endpoint. Keys can be generated only by
|
||||||
can be generated only by specifying the 'role' name. The roles that
|
specifying the 'role' name. The roles that can be used to generate the key for
|
||||||
can be used to generate the key for a particular IP, are listed via
|
a particular IP, are listed via this endpoint. For example, if this backend is
|
||||||
this endpoint. For example, if this backend is mounted at "ssh", then
|
mounted at "ssh", then "ssh/lookup" lists the roles associated with keys can be
|
||||||
"ssh/lookup" lists the roles associated with keys can be generated
|
generated for a target IP, if the CIDR block encompassing the IP is registered
|
||||||
for a target IP, if the CIDR block encompassing the IP, is registered
|
|
||||||
with vault.
|
with vault.
|
||||||
`
|
`
|
||||||
|
|
|
@ -2,7 +2,6 @@ package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
|
@ -14,6 +13,9 @@ const (
|
||||||
KeyTypeDynamic = "dynamic"
|
KeyTypeDynamic = "dynamic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// 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"`
|
KeyName string `mapstructure:"key" json:"key"`
|
||||||
|
@ -140,11 +142,11 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*
|
||||||
if cidrList == "" {
|
if cidrList == "" {
|
||||||
return logical.ErrorResponse("Missing CIDR blocks"), nil
|
return logical.ErrorResponse("Missing CIDR blocks"), nil
|
||||||
}
|
}
|
||||||
for _, item := range strings.Split(cidrList, ",") {
|
|
||||||
_, _, err := net.ParseCIDR(item)
|
// Check if all the CIDR entries are infact valid entries
|
||||||
if err != nil {
|
err := validateCIDRList(cidrList)
|
||||||
return logical.ErrorResponse(fmt.Sprintf("Invalid CIDR list entry '%s'", item)), nil
|
if err != nil {
|
||||||
}
|
return logical.ErrorResponse(fmt.Sprintf("Invalid cidr_list entry. %s", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
port := d.Get("port").(int)
|
port := d.Get("port").(int)
|
||||||
|
@ -158,14 +160,16 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*
|
||||||
}
|
}
|
||||||
keyType = strings.ToLower(keyType)
|
keyType = strings.ToLower(keyType)
|
||||||
|
|
||||||
var err error
|
|
||||||
var roleEntry sshRole
|
var roleEntry sshRole
|
||||||
if keyType == KeyTypeOTP {
|
if keyType == KeyTypeOTP {
|
||||||
|
// 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)
|
adminUser := d.Get("admin_user").(string)
|
||||||
if adminUser != "" {
|
if adminUser != "" {
|
||||||
return logical.ErrorResponse("Admin user not required for OTP type"), nil
|
return logical.ErrorResponse("Admin user not required for OTP type"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Below are the only fields used from the role structure for OTP type.
|
||||||
roleEntry = sshRole{
|
roleEntry = sshRole{
|
||||||
DefaultUser: defaultUser,
|
DefaultUser: defaultUser,
|
||||||
CIDRList: cidrList,
|
CIDRList: cidrList,
|
||||||
|
@ -174,6 +178,7 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*
|
||||||
AllowedUsers: allowedUsers,
|
AllowedUsers: allowedUsers,
|
||||||
}
|
}
|
||||||
} else if keyType == KeyTypeDynamic {
|
} else if keyType == KeyTypeDynamic {
|
||||||
|
// Key name is required by dynamic type and not by OTP type.
|
||||||
keyName := d.Get("key").(string)
|
keyName := d.Get("key").(string)
|
||||||
if keyName == "" {
|
if keyName == "" {
|
||||||
return logical.ErrorResponse("Missing key name"), nil
|
return logical.ErrorResponse("Missing key name"), nil
|
||||||
|
@ -184,10 +189,11 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*
|
||||||
}
|
}
|
||||||
|
|
||||||
installScript := d.Get("install_script").(string)
|
installScript := d.Get("install_script").(string)
|
||||||
|
|
||||||
|
// Setting the default script here. The script will install the
|
||||||
|
// generated public key in the authorized_keys file of linux host.
|
||||||
if installScript == "" {
|
if installScript == "" {
|
||||||
// Setting the default script here. The script will install the generated public key in
|
installScript = DefaultPublicKeyInstallScript
|
||||||
// the authorized_keys file of linux host.
|
|
||||||
installScript = LinuxInstallScript
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adminUser := d.Get("admin_user").(string)
|
adminUser := d.Get("admin_user").(string)
|
||||||
|
@ -195,14 +201,18 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*
|
||||||
return logical.ErrorResponse("Missing admin username"), nil
|
return logical.ErrorResponse("Missing admin username"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Key bits can only be 1024, 2048 or 4096.
|
||||||
keyBits := d.Get("key_bits").(int)
|
keyBits := d.Get("key_bits").(int)
|
||||||
if keyBits != 0 && keyBits != 1024 && keyBits != 2048 && keyBits != 4096 {
|
if keyBits != 0 && keyBits != 1024 && keyBits != 2048 && keyBits != 4096 {
|
||||||
return logical.ErrorResponse("Invalid key_bits field"), nil
|
return logical.ErrorResponse("Invalid key_bits field"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If user has not set this field, default it to 2048
|
||||||
if keyBits == 0 {
|
if keyBits == 0 {
|
||||||
keyBits = 2048
|
keyBits = 2048
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store all the fields required by dynamic key type
|
||||||
roleEntry = sshRole{
|
roleEntry = sshRole{
|
||||||
KeyName: keyName,
|
KeyName: keyName,
|
||||||
AdminUser: adminUser,
|
AdminUser: adminUser,
|
||||||
|
@ -255,24 +265,33 @@ func (b *backend) pathRoleRead(req *logical.Request, d *framework.FieldData) (*l
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return information should be based on the key type of the role
|
||||||
if role.KeyType == KeyTypeOTP {
|
if role.KeyType == KeyTypeOTP {
|
||||||
return &logical.Response{
|
return &logical.Response{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"default_user": role.DefaultUser,
|
"default_user": role.DefaultUser,
|
||||||
"cidr_list": role.CIDRList,
|
"cidr_list": role.CIDRList,
|
||||||
"port": role.Port,
|
"key_type": role.KeyType,
|
||||||
"key_type": role.KeyType,
|
"port": role.Port,
|
||||||
|
"allowed_users": role.AllowedUsers,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
} else {
|
} else {
|
||||||
return &logical.Response{
|
return &logical.Response{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"key": role.KeyName,
|
"key": role.KeyName,
|
||||||
"admin_user": role.AdminUser,
|
"admin_user": role.AdminUser,
|
||||||
"default_user": role.DefaultUser,
|
"default_user": role.DefaultUser,
|
||||||
"cidr_list": role.CIDRList,
|
"cidr_list": role.CIDRList,
|
||||||
"port": role.Port,
|
"port": role.Port,
|
||||||
"key_type": role.KeyType,
|
"key_type": role.KeyType,
|
||||||
|
"key_bits": role.KeyBits,
|
||||||
|
"allowed_users": role.AllowedUsers,
|
||||||
|
// 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,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -292,16 +311,14 @@ Manage the 'roles' that can be created with this backend.
|
||||||
`
|
`
|
||||||
|
|
||||||
const pathRoleHelpDesc = `
|
const pathRoleHelpDesc = `
|
||||||
This path allows you to manage the roles that are used to generate
|
This path allows you to manage the roles that are used to generate credentials.
|
||||||
credentials. These roles will be having privileged access to all
|
|
||||||
the hosts mentioned by CIDR blocks. For example, if the backend
|
|
||||||
is mounted at "ssh" and the role is created at "ssh/roles/web",
|
|
||||||
then a user could request for a new key at "ssh/creds/web" for the
|
|
||||||
supplied username and IP address.
|
|
||||||
|
|
||||||
The 'cidr_list' field takes comma seperated CIDR blocks. The 'admin_user'
|
Role takes a 'key_type' parameter that decides what type of credential this role
|
||||||
should have root access in all the hosts represented by the 'cidr_list'
|
can generate. If remote hosts have Vault SSH Agent installed, an 'otp' type can
|
||||||
field. When the user requests key for an IP, the key will be installed
|
be used, otherwise 'dynamic' type can be used.
|
||||||
for the user mentioned by 'default_user' field. The 'key' field takes
|
|
||||||
a named key which can be configured by 'ssh/keys/' endpoint.
|
If the backend is mounted at "ssh" and the role is created at "ssh/roles/web",
|
||||||
|
then a user could request for a credential at "ssh/creds/web" for an IP that
|
||||||
|
belongs to the role. The credential will be for the 'default_user' registered
|
||||||
|
with the role. There is also an optional parameter 'username' for 'creds/' endpoint.
|
||||||
`
|
`
|
||||||
|
|
|
@ -54,6 +54,9 @@ func (b *backend) pathVerifyWrite(req *logical.Request, d *framework.FieldData)
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the salt of OTP because entry would have been create with the
|
||||||
|
// salt and not directly of the OTP. Salt will yield the same value which
|
||||||
|
// because the seed is the same, the backend salt.
|
||||||
otpSalted := b.salt.SaltID(otp)
|
otpSalted := b.salt.SaltID(otp)
|
||||||
|
|
||||||
// Return nil if there is no entry found for the OTP
|
// Return nil if there is no entry found for the OTP
|
||||||
|
@ -81,14 +84,13 @@ func (b *backend) pathVerifyWrite(req *logical.Request, d *framework.FieldData)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathVerifyHelpSyn = `
|
const pathVerifyHelpSyn = `
|
||||||
Tells if the key provided by the client is valid or not.
|
Validate the OTP provided by Vault SSH Agent.
|
||||||
`
|
`
|
||||||
|
|
||||||
const pathVerifyHelpDesc = `
|
const pathVerifyHelpDesc = `
|
||||||
This path will be used by the vault agent running in the
|
This path will be used by Vault SSH Agent runnin in the remote hosts. The OTP
|
||||||
target machine to check if the key provided by the client
|
provided by the client is sent to Vault for validation by the agent. If Vault
|
||||||
to establish the SSH connection is valid or not.
|
finds an entry for the OTP, it responds with the username and IP it is associated
|
||||||
|
with. Agent uses this information to authenticate the client. Vault deletes the
|
||||||
This key will be a one-time-password. The vault server responds
|
OTP after validating it once.
|
||||||
that the key is valid and then deletes it, hence the key is OTP.
|
|
||||||
`
|
`
|
||||||
|
|
|
@ -112,22 +112,9 @@ func (b *backend) secretDynamicKeyRevoke(req *logical.Request, d *framework.Fiel
|
||||||
return nil, fmt.Errorf("key '%s' not found", hostKeyName)
|
return nil, fmt.Errorf("key '%s' not found", hostKeyName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer the dynamic public key to target machine and use it to remove the entry from authorized_keys file
|
|
||||||
_, dynamicPublicKeyFileName := b.GenerateSaltedOTP()
|
|
||||||
err = scpUpload(adminUser, ip, port, hostKey.Key, dynamicPublicKeyFileName, dynamicPublicKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error uploading pubic key: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptFileName := fmt.Sprintf("%s.sh", dynamicPublicKeyFileName)
|
|
||||||
err = scpUpload(adminUser, ip, port, hostKey.Key, scriptFileName, installScript)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error uploading script file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the public key from authorized_keys file in target machine
|
// Remove the public key from authorized_keys file in target machine
|
||||||
// The last param 'false' indicates that the key should be uninstalled.
|
// The last param 'false' indicates that the key should be uninstalled.
|
||||||
err = installPublicKeyInTarget(adminUser, dynamicPublicKeyFileName, username, ip, port, hostKey.Key, false)
|
err = b.installPublicKeyInTarget(adminUser, username, ip, port, hostKey.Key, dynamicPublicKey, installScript, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error removing public key from authorized_keys file in target")
|
return nil, fmt.Errorf("error removing public key from authorized_keys file in target")
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,9 +57,8 @@ func createSSHPublicKeysSession(username, ipAddr string, port int, hostKey strin
|
||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new RSA key pair with key length of 2048.
|
// Creates a new RSA key pair with the given key length. The private key will be
|
||||||
// The private key will be of pem format and the public key will be
|
// of pem format and the public key will be of OpenSSH format.
|
||||||
// of OpenSSH format.
|
|
||||||
func generateRSAKeys(keyBits int) (publicKeyRsa string, privateKeyRsa string, err error) {
|
func generateRSAKeys(keyBits int) (publicKeyRsa string, privateKeyRsa string, err error) {
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, keyBits)
|
privateKey, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -79,13 +78,32 @@ func generateRSAKeys(keyBits int) (publicKeyRsa string, privateKeyRsa string, er
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Installs or uninstalls the dynamic key in the remote host. The parameterized script
|
// Public key and the script to install the key are uploaded to remote machine.
|
||||||
// will install or uninstall the key. The remote host is assumed to be Linux,
|
// Public key is either added or removed from authorized_keys file using the
|
||||||
// and hence the path of the authorized_keys file is hard coded to resemble Linux.
|
// script. Default script is for a Linux machine and hence the path of the
|
||||||
// Installing and uninstalling the keys means that the public key is appended or
|
// authorized_keys file is hard coded to resemble Linux.
|
||||||
// removed from authorized_keys file.
|
//
|
||||||
// The param 'install' if false, uninstalls the key.
|
// The last param 'install' if false, uninstalls the key.
|
||||||
func installPublicKeyInTarget(adminUser, publicKeyFileName, username, ip string, port int, hostkey string, install bool) error {
|
func (b *backend) installPublicKeyInTarget(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 := b.GenerateSaltedOTP()
|
||||||
|
err := scpUpload(adminUser, ip, port, hostkey, publicKeyFileName, dynamicPublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error uploading public key: %s", 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 = scpUpload(adminUser, ip, port, hostkey, scriptFileName, installScript)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error uploading install script: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a session to run remote command that triggers the script to install
|
||||||
|
// or uninstall the key.
|
||||||
session, err := createSSHPublicKeysSession(adminUser, ip, port, hostkey)
|
session, err := createSSHPublicKeysSession(adminUser, ip, port, hostkey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create SSH Session using public keys: %s", err)
|
return fmt.Errorf("unable to create SSH Session using public keys: %s", err)
|
||||||
|
@ -96,7 +114,6 @@ func installPublicKeyInTarget(adminUser, publicKeyFileName, username, ip string,
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
authKeysFileName := fmt.Sprintf("/home/%s/.ssh/authorized_keys", username)
|
authKeysFileName := fmt.Sprintf("/home/%s/.ssh/authorized_keys", username)
|
||||||
scriptFileName := fmt.Sprintf("%s.sh", publicKeyFileName)
|
|
||||||
|
|
||||||
var installOption string
|
var installOption string
|
||||||
if install {
|
if install {
|
||||||
|
@ -104,6 +121,7 @@ func installPublicKeyInTarget(adminUser, publicKeyFileName, username, ip string,
|
||||||
} else {
|
} else {
|
||||||
installOption = "uninstall"
|
installOption = "uninstall"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give execute permissions to install script, run and delete it.
|
// Give execute permissions to install script, run and delete it.
|
||||||
chmodCmd := fmt.Sprintf("chmod +x %s", scriptFileName)
|
chmodCmd := fmt.Sprintf("chmod +x %s", scriptFileName)
|
||||||
scriptCmd := fmt.Sprintf("./%s %s %s %s", scriptFileName, installOption, publicKeyFileName, authKeysFileName)
|
scriptCmd := fmt.Sprintf("./%s %s %s %s", scriptFileName, installOption, publicKeyFileName, authKeysFileName)
|
||||||
|
@ -145,6 +163,17 @@ func roleContainsIP(s logical.Storage, roleName string, ip string) (bool, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if the comma separated list of CIDR blocks are all valid.
|
||||||
|
func validateCIDRList(cidrList string) error {
|
||||||
|
for _, item := range strings.Split(cidrList, ",") {
|
||||||
|
_, _, err := net.ParseCIDR(item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Returns true if the IP supplied by the user is part of the comma
|
// Returns true if the IP supplied by the user is part of the comma
|
||||||
// separated CIDR blocks
|
// separated CIDR blocks
|
||||||
func cidrContainsIP(ip, cidrList string) (bool, error) {
|
func cidrContainsIP(ip, cidrList string) (bool, error) {
|
||||||
|
@ -160,6 +189,7 @@ func cidrContainsIP(ip, cidrList string) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uploads the file to the remote machine
|
||||||
func scpUpload(username, ip string, port int, hostkey, fileName, fileContent string) error {
|
func scpUpload(username, ip string, port int, hostkey, fileName, fileContent string) error {
|
||||||
signer, err := ssh.ParsePrivateKey([]byte(hostkey))
|
signer, err := ssh.ParsePrivateKey([]byte(hostkey))
|
||||||
clientConfig := &ssh.ClientConfig{
|
clientConfig := &ssh.ClientConfig{
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/builtin/logical/ssh"
|
"github.com/hashicorp/vault/builtin/logical/ssh"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SSHCommand is a Command that establishes a SSH connection
|
// SSHCommand is a Command that establishes a SSH connection
|
||||||
|
@ -19,8 +20,16 @@ type SSHCommand struct {
|
||||||
Meta
|
Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Structure to hold the fields returned when asked for a credential from SSHh backend.
|
||||||
|
type SSHCredentialResp struct {
|
||||||
|
KeyType string `mapstructure:"key_type"`
|
||||||
|
Key string `mapstructure:"key"`
|
||||||
|
Username string `mapstructure:"username"`
|
||||||
|
IP string `mapstructure:"ip"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
func (c *SSHCommand) Run(args []string) int {
|
func (c *SSHCommand) Run(args []string) int {
|
||||||
var portNum int
|
|
||||||
var role, mountPoint, format string
|
var role, mountPoint, format string
|
||||||
var noExec bool
|
var noExec bool
|
||||||
var sshCmdArgs []string
|
var sshCmdArgs []string
|
||||||
|
@ -28,7 +37,6 @@ func (c *SSHCommand) Run(args []string) int {
|
||||||
flags := c.Meta.FlagSet("ssh", FlagSetDefault)
|
flags := c.Meta.FlagSet("ssh", FlagSetDefault)
|
||||||
flags.StringVar(&format, "format", "table", "")
|
flags.StringVar(&format, "format", "table", "")
|
||||||
flags.StringVar(&role, "role", "", "")
|
flags.StringVar(&role, "role", "", "")
|
||||||
flags.IntVar(&portNum, "port", 22, "")
|
|
||||||
flags.StringVar(&mountPoint, "mount-point", "ssh", "")
|
flags.StringVar(&mountPoint, "mount-point", "ssh", "")
|
||||||
flags.BoolVar(&noExec, "no-exec", false, "")
|
flags.BoolVar(&noExec, "no-exec", false, "")
|
||||||
|
|
||||||
|
@ -42,8 +50,6 @@ func (c *SSHCommand) Run(args []string) int {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
port := strconv.Itoa(portNum)
|
|
||||||
|
|
||||||
client, err := c.Client()
|
client, err := c.Client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||||
|
@ -115,17 +121,24 @@ func (c *SSHCommand) Run(args []string) int {
|
||||||
return OutputSecret(c.Ui, format, keySecret)
|
return OutputSecret(c.Ui, format, keySecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
if keySecret.Data["key_type"].(string) == ssh.KeyTypeDynamic {
|
var resp SSHCredentialResp
|
||||||
sshDynamicKey := string(keySecret.Data["key"].(string))
|
if err := mapstructure.Decode(keySecret.Data, &resp); err != nil {
|
||||||
if len(sshDynamicKey) == 0 {
|
c.Ui.Error(fmt.Sprintf("Error parsing the credential response:%s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
port := strconv.Itoa(resp.Port)
|
||||||
|
|
||||||
|
if resp.KeyType == ssh.KeyTypeDynamic {
|
||||||
|
if len(resp.Key) == 0 {
|
||||||
c.Ui.Error(fmt.Sprintf("Invalid key"))
|
c.Ui.Error(fmt.Sprintf("Invalid key"))
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
sshDynamicKeyFileName = fmt.Sprintf("vault_ssh_%s_%s", username, ip.String())
|
sshDynamicKeyFileName = fmt.Sprintf("vault_ssh_%s_%s", username, ip.String())
|
||||||
err = ioutil.WriteFile(sshDynamicKeyFileName, []byte(sshDynamicKey), 0600)
|
err = ioutil.WriteFile(sshDynamicKeyFileName, []byte(resp.Key), 0600)
|
||||||
sshCmdArgs = append(sshCmdArgs, []string{"-i", sshDynamicKeyFileName}...)
|
sshCmdArgs = append(sshCmdArgs, []string{"-i", sshDynamicKeyFileName}...)
|
||||||
|
|
||||||
} else if keySecret.Data["key_type"].(string) == ssh.KeyTypeOTP {
|
} else if resp.KeyType == ssh.KeyTypeOTP {
|
||||||
// Check if the application 'sshpass' is installed in the client machine.
|
// Check if the application 'sshpass' is installed in the client machine.
|
||||||
// If it is then, use it to automate typing in OTP to the prompt. Unfortunately,
|
// If it is then, use it to automate typing in OTP to the prompt. Unfortunately,
|
||||||
// it was not possible to automate it without a third-party application, with
|
// it was not possible to automate it without a third-party application, with
|
||||||
|
@ -133,7 +146,7 @@ func (c *SSHCommand) Run(args []string) int {
|
||||||
// Feel free to try and remove this dependency.
|
// Feel free to try and remove this dependency.
|
||||||
sshpassPath, err := exec.LookPath("sshpass")
|
sshpassPath, err := exec.LookPath("sshpass")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
sshCmdArgs = append(sshCmdArgs, []string{"-p", string(keySecret.Data["key"].(string)), "ssh", "-p", port}...)
|
sshCmdArgs = append(sshCmdArgs, []string{"-p", string(resp.Key), "ssh", "-p", port}...)
|
||||||
sshCmdArgs = append(sshCmdArgs, args...)
|
sshCmdArgs = append(sshCmdArgs, args...)
|
||||||
sshCmd := exec.Command(sshpassPath, sshCmdArgs...)
|
sshCmd := exec.Command(sshpassPath, sshCmdArgs...)
|
||||||
sshCmd.Stdin = os.Stdin
|
sshCmd.Stdin = os.Stdin
|
||||||
|
@ -144,7 +157,7 @@ func (c *SSHCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
c.Ui.Output("OTP for the session is " + string(keySecret.Data["key"].(string)))
|
c.Ui.Output("OTP for the session is " + resp.Key)
|
||||||
c.Ui.Output("[Note: Install 'sshpass' to automate typing in OTP]")
|
c.Ui.Output("[Note: Install 'sshpass' to automate typing in OTP]")
|
||||||
}
|
}
|
||||||
sshCmdArgs = append(sshCmdArgs, []string{"-p", port}...)
|
sshCmdArgs = append(sshCmdArgs, []string{"-p", port}...)
|
||||||
|
@ -164,7 +177,7 @@ func (c *SSHCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the temporary key file generated by the command.
|
// Delete the temporary key file generated by the command.
|
||||||
if keySecret.Data["key_type"].(string) == ssh.KeyTypeDynamic {
|
if resp.KeyType == ssh.KeyTypeDynamic {
|
||||||
// Ignoring the error from the below call since it is not a security
|
// Ignoring the error from the below call since it is not a security
|
||||||
// issue if the deletion of file is not successful. User is authorized
|
// issue if the deletion of file is not successful. User is authorized
|
||||||
// to have this secret.
|
// to have this secret.
|
||||||
|
@ -256,8 +269,6 @@ SSH Options:
|
||||||
If there are no roles associated with the IP, register
|
If there are no roles associated with the IP, register
|
||||||
the CIDR block of that IP using the "roles/" endpoint.
|
the CIDR block of that IP using the "roles/" endpoint.
|
||||||
|
|
||||||
-port Port number to use for SSH connection. This defaults to port 22.
|
|
||||||
|
|
||||||
-no-exec Shows the credentials but does not establish connection.
|
-no-exec Shows the credentials but does not establish connection.
|
||||||
|
|
||||||
-mount-point Mount point of SSH backend. If the backend is mounted at
|
-mount-point Mount point of SSH backend. If the backend is mounted at
|
||||||
|
|
|
@ -10,11 +10,19 @@ description: |-
|
||||||
|
|
||||||
Name: `ssh`
|
Name: `ssh`
|
||||||
|
|
||||||
The SSH secret backend for Vault generates SSH credentials dynamically. This backend
|
Vault SSH backend generates SSH credentials for remote hosts dynamically. This
|
||||||
increases the security and solves the problem of management and distribution of keys
|
backend increases the security by removing the need to share the private key to
|
||||||
belonging to remote hosts. This backend provides two ways of credential creation.
|
everyone who needs access to infrastructures. It also solves the problem of
|
||||||
Both of them addresses the problem in different ways. Understand both of them and
|
management and distribution of keys belonging to remote hosts.
|
||||||
choose the one which best suits your needs.
|
|
||||||
|
This backend supports two types of credential creation: Dynamic and OTP. Both of
|
||||||
|
them addresses the problems in different ways.
|
||||||
|
|
||||||
|
Read and carefully understand both of them and choose the one which best suits
|
||||||
|
your needs.
|
||||||
|
|
||||||
|
This page will show a quick start for this backend. For detailed documentation
|
||||||
|
on every path, use `vault path-help` after mounting the backend.
|
||||||
|
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
## I. Dynamic Type
|
## I. Dynamic Type
|
||||||
|
@ -23,13 +31,12 @@ Register the shared secret key (having super user privileges) with Vault and let
|
||||||
Vault take care of issuing a dynamic secret key every time a client wants to SSH
|
Vault take care of issuing a dynamic secret key every time a client wants to SSH
|
||||||
into the remote host.
|
into the remote host.
|
||||||
|
|
||||||
When a Vault authenticated client requests for a credential, Vault server creates
|
When a Vault authenticated client requests for a dynamic credential, Vault server
|
||||||
a key-pair, uses the previously shared secret key to login to the remote host and
|
creates a key-pair, uses the previously shared secret key to login to the remote
|
||||||
appends the newly generated public key to ~/.ssh/authorized_keys file of the desired
|
host and appends the newly generated public key to ~/.ssh/authorized_keys file for
|
||||||
username. Vault uses a install script (configurable) to achieve this.
|
the desired username. Vault uses an install script (configurable) to achieve this.
|
||||||
|
To run this script in super user mode without password prompts, `NOPASSWD` option
|
||||||
To run the script without prompts, password requests for sudoers should be disabled at
|
for sudoers should be enabled at all remote hosts.
|
||||||
remote hosts.
|
|
||||||
|
|
||||||
File: `/etc/sudoers`
|
File: `/etc/sudoers`
|
||||||
|
|
||||||
|
@ -38,9 +45,10 @@ File: `/etc/sudoers`
|
||||||
```
|
```
|
||||||
|
|
||||||
The private key returned to the user will be leased and can be renewed if desired.
|
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 has no control on how and when the keys
|
Once the key is given to the user, Vault will not know when the user it or how many
|
||||||
will be used. Therefore, Vault **WILL NOT** and cannot audit the SSH session establishments.
|
time it gets used. Therefore, Vault **WILL NOT** and cannot audit the SSH session
|
||||||
OTP type audits every SSH request (see below).
|
establishments. An alternative is to use OTP type, which audits every SSH request
|
||||||
|
(see below).
|
||||||
|
|
||||||
### Mounting SSH
|
### Mounting SSH
|
||||||
|
|
||||||
|
@ -74,8 +82,9 @@ Success! Data written to: ssh/roles/dynamic_key_role
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the `install_script` option to provide an install script if hosts does not
|
Use the `install_script` option to provide an install script if hosts does not
|
||||||
resemble typical Linux machine. The default script is very straight forward and
|
resemble typical Linux machine. The default script is compiled into the binary.
|
||||||
is shown below. The script takes three arguments which are explained in the comments.
|
It is straight forward and is shown below. The script takes three arguments which
|
||||||
|
are explained in the comments.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# This script file installs or uninstalls an RSA public key to/from authoried_keys
|
# This script file installs or uninstalls an RSA public key to/from authoried_keys
|
||||||
|
@ -276,13 +285,15 @@ username@ip:~$
|
||||||
<li>
|
<li>
|
||||||
<span class="param">lease</span>
|
<span class="param">lease</span>
|
||||||
<span class="param-flags">required</span>
|
<span class="param-flags">required</span>
|
||||||
(String) The lease value provided as a duration
|
(String)
|
||||||
|
The lease value provided as a duration
|
||||||
with time suffix. Hour is the largest suffix.
|
with time suffix. Hour is the largest suffix.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="param">lease_max</span>
|
<span class="param">lease_max</span>
|
||||||
<span class="param-flags">required</span>
|
<span class="param-flags">required</span>
|
||||||
(String) The maximum lease value provided as a duration
|
(String)
|
||||||
|
The maximum lease value provided as a duration
|
||||||
with time suffix. Hour is the largest suffix.
|
with time suffix. Hour is the largest suffix.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -315,7 +326,8 @@ username@ip:~$
|
||||||
<li>
|
<li>
|
||||||
<span class="param">key</span>
|
<span class="param">key</span>
|
||||||
<span class="param-flags">required</span>
|
<span class="param-flags">required</span>
|
||||||
(String) SSH private key with super user privileges in host
|
(String)
|
||||||
|
SSH private key with super user privileges in host
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</dd>
|
</dd>
|
||||||
|
@ -396,12 +408,14 @@ username@ip:~$
|
||||||
<li>
|
<li>
|
||||||
<span class="param">key</span>
|
<span class="param">key</span>
|
||||||
<span class="param-flags">required for dynamic type, NA for otp type</span>
|
<span class="param-flags">required for dynamic type, NA for otp type</span>
|
||||||
Name of the registered key in Vault. Before creating the role, use the `keys/`
|
(String)
|
||||||
endpoint to create a named key.
|
Name of the registered key in Vault. Before creating the role, use
|
||||||
|
the `keys/` endpoint to create a named key.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="param">admin_user</span>
|
<span class="param">admin_user</span>
|
||||||
<span class="param-flags">required for dynamic type, NA for otp type</span>
|
<span class="param-flags">required for dynamic type, NA for otp type</span>
|
||||||
|
(String)
|
||||||
Admin user at remote host. The shared key being registered should be
|
Admin user at remote host. The shared key being registered should be
|
||||||
for this user and should have root privileges. Everytime a dynamic
|
for this user and should have root privileges. Everytime a dynamic
|
||||||
credential is being generated for other users, Vault uses this admin
|
credential is being generated for other users, Vault uses this admin
|
||||||
|
@ -411,6 +425,7 @@ username@ip:~$
|
||||||
<li>
|
<li>
|
||||||
<span class="param">default_user</span>
|
<span class="param">default_user</span>
|
||||||
<span class="param-flags">required for both types</span>
|
<span class="param-flags">required for both types</span>
|
||||||
|
(String)
|
||||||
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.
|
||||||
|
@ -418,12 +433,14 @@ username@ip:~$
|
||||||
<li>
|
<li>
|
||||||
<span class="param">cidr_list</span>
|
<span class="param">cidr_list</span>
|
||||||
<span class="param-flags">required for both types</span>
|
<span class="param-flags">required for both types</span>
|
||||||
|
(String)
|
||||||
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.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="param">port</span>
|
<span class="param">port</span>
|
||||||
<span class="param-flags">optional for both types</span>
|
<span class="param-flags">optional for both types</span>
|
||||||
|
(Integer)
|
||||||
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
|
||||||
|
@ -432,23 +449,27 @@ username@ip:~$
|
||||||
<li>
|
<li>
|
||||||
<span class="param">key_type</span>
|
<span class="param">key_type</span>
|
||||||
<span class="param-flags">required for both types</span>
|
<span class="param-flags">required for both types</span>
|
||||||
|
(String)
|
||||||
Type of key used to login to hosts. It can be either `otp` or `dynamic`.
|
Type of key used to login to hosts. It can be either `otp` or `dynamic`.
|
||||||
`otp` type requires agent to be installed in remote hosts.
|
`otp` type requires agent to be installed in remote hosts.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="param">key_bits</span>
|
<span class="param">key_bits</span>
|
||||||
<span class="param-flags">optional for dynamic type, NA for otp type</span>
|
<span class="param-flags">optional for dynamic type, NA for otp type</span>
|
||||||
|
(Integer)
|
||||||
Length of the RSA dynamic key in bits. It can be one of 1024, 2048 or 4096.
|
Length of the RSA dynamic key in bits. It can be one of 1024, 2048 or 4096.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="param">install_script</span>
|
<span class="param">install_script</span>
|
||||||
<span class="param-flags">optional for dynamic type, NA for otp type</span>
|
<span class="param-flags">optional for dynamic type, NA for otp type</span>
|
||||||
|
(String)
|
||||||
Script used to install and uninstall public keys in the target machine.
|
Script used to install and uninstall public keys in the target machine.
|
||||||
The inbuilt default install script will be for Linux hosts.
|
The inbuilt default install script will be for Linux hosts.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="param">allowed_users</span>
|
<span class="param">allowed_users</span>
|
||||||
<span class="param-flags">optional for both types</span>
|
<span class="param-flags">optional for both types</span>
|
||||||
|
(String)
|
||||||
If this option is not specified, client can request for a credential for
|
If this option is not specified, client can request for a credential for
|
||||||
any valid user at the remote host, including the admin user. If only certain
|
any valid user at the remote host, including the admin user. If only certain
|
||||||
usernames are to be allowed, then this list enforces it. If this field is
|
usernames are to be allowed, then this list enforces it. If this field is
|
||||||
|
@ -550,11 +571,13 @@ username@ip:~$
|
||||||
<li>
|
<li>
|
||||||
<span class="param">username</span>
|
<span class="param">username</span>
|
||||||
<span class="param-flags">optional</span>
|
<span class="param-flags">optional</span>
|
||||||
|
(String)
|
||||||
Username in remote host.
|
Username in remote host.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="param">ip</span>
|
<span class="param">ip</span>
|
||||||
<span class="param-flags">required</span>
|
<span class="param-flags">required</span>
|
||||||
|
(String)
|
||||||
IP of the remote host.
|
IP of the remote host.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -586,6 +609,7 @@ username@ip:~$
|
||||||
<li>
|
<li>
|
||||||
<span class="param">ip</span>
|
<span class="param">ip</span>
|
||||||
<span class="param-flags">required</span>
|
<span class="param-flags">required</span>
|
||||||
|
(String)
|
||||||
IP of the remote host.
|
IP of the remote host.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -617,6 +641,7 @@ username@ip:~$
|
||||||
<li>
|
<li>
|
||||||
<span class="param">otp</span>
|
<span class="param">otp</span>
|
||||||
<span class="param-flags">required</span>
|
<span class="param-flags">required</span>
|
||||||
|
(String)
|
||||||
One-Time-Key that needs to be validated.
|
One-Time-Key that needs to be validated.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
Loading…
Reference in New Issue