Vault SSH: Review Rework

This commit is contained in:
vishalnayak 2015-07-29 14:21:36 -04:00
parent e6a99525dc
commit 61c9f884a4
11 changed files with 282 additions and 247 deletions

View File

@ -4,27 +4,21 @@ import "fmt"
// SSH is used to return a client to invoke operations on SSH backend.
type SSH struct {
c *Client
c *Client
Path string
}
// SSH is used to return the client for logical-backend API calls.
func (c *Client) SSH() *SSH {
return &SSH{c: c}
}
// Invokes the SSH backend API to revoke a key identified by its lease ID.
func (c *SSH) KeyRevoke(id string) error {
r := c.c.NewRequest("PUT", "/v1/sys/revoke/"+id)
resp, err := c.c.RawRequest(r)
if err == nil {
defer resp.Body.Close()
func (c *Client) SSH(path string) *SSH {
return &SSH{
c: c,
Path: path,
}
return err
}
// Invokes the SSH backend API to create a dynamic key or an OTP
func (c *SSH) KeyCreate(role string, data map[string]interface{}) (*Secret, error) {
r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/ssh/creds/%s", role))
func (c *SSH) Credential(role string, data map[string]interface{}) (*Secret, error) {
r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/%s/creds/%s", c.Path, role))
if err := r.SetJSONBody(data); err != nil {
return nil, err
}

View File

@ -8,6 +8,11 @@ import (
"github.com/hashicorp/vault/logical/framework"
)
type backend struct {
*framework.Backend
salt *salt.Salt
}
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
b, err := Backend(conf)
if err != nil {
@ -54,11 +59,6 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) {
return b.Backend, nil
}
type backend struct {
*framework.Backend
salt *salt.Salt
}
const backendHelp = `
The SSH backend generates keys to eatablish SSH connection
with remote hosts. There are two options to create the keys:

View File

@ -8,6 +8,11 @@ import (
"github.com/hashicorp/vault/logical/framework"
)
type configLease struct {
Lease time.Duration
LeaseMax time.Duration
}
func pathConfigLease(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/lease",
@ -89,11 +94,6 @@ func (b *backend) Lease(s logical.Storage) (*configLease, error) {
return &result, nil
}
type configLease struct {
Lease time.Duration
LeaseMax time.Duration
}
const pathConfigLeaseHelpSyn = `
Configure the default lease information for SSH dynamic keys.
`

View File

@ -3,12 +3,22 @@ package ssh
import (
"fmt"
"net"
"strconv"
"github.com/hashicorp/vault/helper/uuid"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
type sshOTP struct {
Username string `json:"username"`
IP string `json:"ip"`
}
type sshCIDR struct {
CIDR []string
}
func pathCredsCreate(b *backend) *framework.Path {
return &framework.Path{
Pattern: "creds/(?P<name>[-\\w]+)",
@ -85,25 +95,10 @@ func (b *backend) pathCredsCreateWrite(
var result *logical.Response
if role.KeyType == KeyTypeOTP {
// Generate salted OTP
otp := uuid.GenerateUUID()
otpSalted := b.salt.SaltID(otp)
entry, err := req.Storage.Get("otp/" + otpSalted)
// Make sure that new OTP is not replacing an existing one
for err == nil && entry != nil {
otp := uuid.GenerateUUID()
otpSalted := b.salt.SaltID(otp)
entry, err = req.Storage.Get("otp/" + otpSalted)
}
entry, err = logical.StorageEntryJSON("otp/"+otpSalted, sshOTP{
Username: username,
IP: ip,
})
otp, err := b.GenerateOTPCredential(req, username, ip)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
result = b.Secret(SecretOTPType).Response(map[string]interface{}{
"key_type": role.KeyType,
"key": otp,
@ -111,39 +106,10 @@ func (b *backend) pathCredsCreateWrite(
"otp": otp,
})
} else if role.KeyType == KeyTypeDynamic {
// Fetch the host key to be used for dynamic key installation
keyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", role.KeyName))
if err != nil {
return nil, fmt.Errorf("key '%s' not found error:%s", role.KeyName, err)
}
if keyEntry == nil {
return nil, fmt.Errorf("key '%s' not found", role.KeyName, err)
}
var hostKey sshHostKey
if err := keyEntry.DecodeJSON(&hostKey); err != nil {
return nil, fmt.Errorf("error reading the host key: %s", err)
}
// Generate RSA key pair
dynamicPublicKey, dynamicPrivateKey, err := generateRSAKeys()
if err != nil {
return nil, fmt.Errorf("error generating key: %s", err)
}
// Transfer the public key to target machine
err = uploadPublicKeyScp(dynamicPublicKey, username, ip, role.Port, hostKey.Key)
dynamicPublicKey, dynamicPrivateKey, err := b.GenerateDynamicCredential(req, &role, username, ip)
if err != nil {
return nil, err
}
// Add the public key to authorized_keys file in target machine
err = installPublicKeyInTarget(role.AdminUser, username, ip, role.Port, hostKey.Key)
if err != nil {
return nil, fmt.Errorf("error adding public key to authorized_keys file in target")
}
result = b.Secret(SecretDynamicKeyType).Response(map[string]interface{}{
"key": dynamicPrivateKey,
"key_type": role.KeyType,
@ -168,13 +134,74 @@ func (b *backend) pathCredsCreateWrite(
return result, nil
}
type sshOTP struct {
Username string `json:"username"`
IP string `json:"ip"`
// Generates a RSA key pair and installs it in the remote target
func (b *backend) GenerateDynamicCredential(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(fmt.Sprintf("keys/%s", role.KeyName))
if err != nil {
return "", "", fmt.Errorf("key '%s' not found error:%s", role.KeyName, err)
}
if keyEntry == nil {
return "", "", fmt.Errorf("key '%s' not found", role.KeyName, err)
}
var hostKey sshHostKey
if err := keyEntry.DecodeJSON(&hostKey); err != nil {
return "", "", fmt.Errorf("error reading the host key: %s", err)
}
// Generate RSA key pair
keyBits, err := strconv.Atoi(role.KeyBits)
if err != nil {
return "", "", fmt.Errorf("error reading key bit size: %s", err)
}
dynamicPublicKey, dynamicPrivateKey, err := generateRSAKeys(keyBits)
if err != nil {
return "", "", fmt.Errorf("error generating key: %s", err)
}
// Transfer the public key to target machine
publicKeyFileName := uuid.GenerateUUID()
err = uploadPublicKeyScp(dynamicPublicKey, publicKeyFileName, username, ip, role.Port, hostKey.Key)
if err != nil {
return "", "", err
}
// Add the public key to authorized_keys file in target machine
err = installPublicKeyInTarget(role.AdminUser, publicKeyFileName, username, ip, role.Port, hostKey.Key)
if err != nil {
return "", "", fmt.Errorf("error adding public key to authorized_keys file in target")
}
return dynamicPublicKey, dynamicPrivateKey, nil
}
type sshCIDR struct {
CIDR []string
// Generates an OTP and creates an entry for the same in storage backend.
func (b *backend) GenerateOTPCredential(req *logical.Request, username, ip string) (string, error) {
otp := uuid.GenerateUUID()
otpSalted := b.salt.SaltID(otp)
entry, err := req.Storage.Get("otp/" + otpSalted)
// Make sure that new OTP is not replacing an existing one
for err == nil && entry != nil {
otp = uuid.GenerateUUID()
otpSalted = b.salt.SaltID(otp)
entry, err = req.Storage.Get("otp/" + otpSalted)
if err != nil {
return "", err
}
}
entry, err = logical.StorageEntryJSON("otp/"+otpSalted, sshOTP{
Username: username,
IP: ip,
})
if err != nil {
return "", err
}
if err := req.Storage.Put(entry); err != nil {
return "", err
}
return otp, nil
}
const pathCredsCreateHelpSyn = `

View File

@ -3,10 +3,16 @@ package ssh
import (
"fmt"
"golang.org/x/crypto/ssh"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
type sshHostKey struct {
Key string
}
func pathKeys(b *backend) *framework.Path {
return &framework.Path{
Pattern: "keys/(?P<name>[-\\w]+)",
@ -62,6 +68,11 @@ func (b *backend) pathKeysWrite(req *logical.Request, d *framework.FieldData) (*
keyName := d.Get("name").(string)
keyString := d.Get("key").(string)
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
}
@ -80,10 +91,6 @@ func (b *backend) pathKeysWrite(req *logical.Request, d *framework.FieldData) (*
return nil, nil
}
type sshHostKey struct {
Key string
}
const pathKeysSyn = `
Register a shared key which can be used to install dynamic key
in remote machine.

View File

@ -3,12 +3,17 @@ package ssh
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
const KeyTypeOTP = "otp"
const KeyTypeDynamic = "dynamic"
const KeyBitsRSA = "2048"
func pathRoles(b *backend) *framework.Path {
return &framework.Path{
Pattern: "roles/(?P<name>[-\\w]+)",
@ -41,6 +46,10 @@ func pathRoles(b *backend) *framework.Path {
Type: framework.TypeString,
Description: "one-time-password or dynamic-key",
},
"key_bits": &framework.FieldSchema{
Type: framework.TypeString,
Description: "number of bits in keys",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -54,127 +63,104 @@ func pathRoles(b *backend) *framework.Path {
}
}
func createOTPRole(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
roleName := d.Get("name").(string)
if roleName == "" {
return logical.ErrorResponse("Missing role name"), nil
}
cidr := d.Get("cidr").(string)
if cidr == "" {
return logical.ErrorResponse("Missing cidr blocks"), nil
}
for _, item := range strings.Split(cidr, ",") {
_, _, err := net.ParseCIDR(item)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Invalid cidr entry '%s'", item)), nil
}
}
adminUser := d.Get("admin_user").(string)
defaultUser := d.Get("default_user").(string)
if defaultUser == "" && adminUser != "" {
defaultUser = adminUser
}
port := d.Get("port").(string)
if port == "" {
port = "22"
}
entry, err := logical.StorageEntryJSON(fmt.Sprintf("policy/%s", roleName), sshRole{
AdminUser: adminUser,
DefaultUser: defaultUser,
CIDR: cidr,
Port: port,
KeyType: KeyTypeOTP,
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
func createDynamicKeyRole(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
roleName := d.Get("name").(string)
if roleName == "" {
return logical.ErrorResponse("Missing role name"), nil
}
keyName := d.Get("key").(string)
if keyName == "" {
return logical.ErrorResponse("Missing key name"), nil
}
keyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", keyName))
if err != nil || keyEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("Invalid 'key': '%s'", keyName)), nil
}
adminUser := d.Get("admin_user").(string)
if adminUser == "" {
return logical.ErrorResponse("Missing admin username"), nil
}
cidr := d.Get("cidr").(string)
if cidr == "" {
return logical.ErrorResponse("Missing cidr blocks"), nil
}
for _, item := range strings.Split(cidr, ",") {
_, _, err := net.ParseCIDR(item)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Invalid cidr entry '%s'", item)), nil
}
}
defaultUser := d.Get("default_user").(string)
if defaultUser == "" {
defaultUser = adminUser
}
port := d.Get("port").(string)
if port == "" {
port = "22"
}
entry, err := logical.StorageEntryJSON(fmt.Sprintf("policy/%s", roleName), sshRole{
KeyName: keyName,
AdminUser: adminUser,
DefaultUser: defaultUser,
CIDR: cidr,
Port: port,
KeyType: KeyTypeDynamic,
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
roleName := d.Get("name").(string)
if roleName == "" {
return logical.ErrorResponse("Missing role name"), nil
}
cidr := d.Get("cidr").(string)
if cidr == "" {
return logical.ErrorResponse("Missing cidr blocks"), nil
}
for _, item := range strings.Split(cidr, ",") {
_, _, err := net.ParseCIDR(item)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Invalid cidr entry '%s'", item)), nil
}
}
port := d.Get("port").(string)
if port == "" {
port = "22"
}
keyType := d.Get("key_type").(string)
if keyType == "" {
return logical.ErrorResponse("Missing key type"), nil
}
keyType = strings.ToLower(keyType)
var entry *logical.StorageEntry
var err error
if keyType == KeyTypeOTP {
return createOTPRole(req, d)
adminUser := d.Get("admin_user").(string)
if adminUser != "" {
return logical.ErrorResponse("Admin user not required for OTP type"), nil
}
defaultUser := d.Get("default_user").(string)
if defaultUser == "" {
return logical.ErrorResponse("Missing default user"), nil
}
entry, err = logical.StorageEntryJSON(fmt.Sprintf("policy/%s", roleName), sshRole{
DefaultUser: defaultUser,
CIDR: cidr,
KeyType: KeyTypeOTP,
})
} else if keyType == KeyTypeDynamic {
return createDynamicKeyRole(req, d)
keyName := d.Get("key").(string)
if keyName == "" {
return logical.ErrorResponse("Missing key name"), nil
}
keyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", keyName))
if err != nil || keyEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("Invalid 'key': '%s'", keyName)), nil
}
adminUser := d.Get("admin_user").(string)
if adminUser == "" {
return logical.ErrorResponse("Missing admin username"), nil
}
defaultUser := d.Get("default_user").(string)
if defaultUser == "" {
defaultUser = adminUser
}
keyBits := d.Get("key_bits").(string)
if keyBits != "" {
_, err := strconv.Atoi(keyBits)
if err != nil {
return logical.ErrorResponse("Key bits should be an integer"), nil
}
}
if keyBits == "" {
keyBits = KeyBitsRSA
}
entry, err = logical.StorageEntryJSON(fmt.Sprintf("policy/%s", roleName), sshRole{
KeyName: keyName,
AdminUser: adminUser,
DefaultUser: defaultUser,
CIDR: cidr,
Port: port,
KeyType: KeyTypeDynamic,
KeyBits: keyBits,
})
} else {
return logical.ErrorResponse("Invalid key type"), nil
}
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathRoleRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
@ -216,15 +202,13 @@ func (b *backend) pathRoleDelete(req *logical.Request, d *framework.FieldData) (
type sshRole struct {
KeyType string `json:"key_type"`
KeyName string `json:"key"`
KeyBits string `json:"key_bits"`
AdminUser string `json:"admin_user"`
DefaultUser string `json:"default_user"`
CIDR string `json:"cidr"`
Port string `json:"port"`
}
const KeyTypeOTP = "otp"
const KeyTypeDynamic = "dynamic"
const pathRoleHelpSyn = `
Manage the 'roles' that can be created with this backend.
`

View File

@ -41,11 +41,11 @@ func (b *backend) pathVerifyWrite(req *logical.Request, d *framework.FieldData)
if err != nil {
return nil, err
}
return &logical.Response{
Data: map[string]interface{}{
"username": otpEntry.Username,
"ip": otpEntry.IP,
"valid": "yes",
},
}, nil
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/hashicorp/vault/helper/uuid"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
@ -24,7 +25,7 @@ func secretDynamicKey(b *backend) *framework.Secret {
},
},
DefaultDuration: 10 * time.Minute,
DefaultGracePeriod: 5 * time.Minute,
DefaultGracePeriod: 2 * time.Minute,
Renew: b.secretDynamicKeyRenew,
Revoke: b.secretDynamicKeyRevoke,
}
@ -105,13 +106,14 @@ func (b *backend) secretDynamicKeyRevoke(req *logical.Request, d *framework.Fiel
}
// Transfer the dynamic public key to target machine and use it to remove the entry from authorized_keys file
err = uploadPublicKeyScp(dynamicPublicKey, username, ip, port, hostKey.Key)
dynamicPublicKeyFileName := uuid.GenerateUUID()
err = uploadPublicKeyScp(dynamicPublicKey, dynamicPublicKeyFileName, username, ip, port, hostKey.Key)
if err != nil {
return nil, fmt.Errorf("public key transfer failed: %s", err)
}
// Remove the public key from authorized_keys file in target machine
err = uninstallPublicKeyInTarget(adminUser, username, ip, port, hostKey.Key)
err = uninstallPublicKeyInTarget(adminUser, dynamicPublicKeyFileName, username, ip, port, hostKey.Key)
if err != nil {
return nil, fmt.Errorf("error removing public key from authorized_keys file in target")
}

View File

@ -20,24 +20,11 @@ func secretOTP(b *backend) *framework.Secret {
},
},
DefaultDuration: 10 * time.Minute,
DefaultGracePeriod: 5 * time.Minute,
Renew: b.secretOTPRenew,
DefaultGracePeriod: 2 * time.Minute,
Revoke: b.secretOTPRevoke,
}
}
func (b *backend) secretOTPRenew(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
lease, err := b.Lease(req.Storage)
if err != nil {
return nil, err
}
if lease == nil {
lease = &configLease{Lease: 1 * time.Hour}
}
f := framework.LeaseExtend(lease.Lease, lease.LeaseMax, false)
return f(req, d)
}
func (b *backend) secretOTPRevoke(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
otpRaw, ok := req.Secret.InternalData["otp"]
if !ok {

View File

@ -20,8 +20,7 @@ import (
// session with the target. Uses the public key authentication method
// and hence the parameter 'key' takes in the private key. The fileName
// parameter takes an absolute path.
func uploadPublicKeyScp(publicKey, username, ip, port, key string) error {
dynamicPublicKeyFileName := fmt.Sprintf("vault_ssh_%s_%s.pub", username, ip)
func uploadPublicKeyScp(publicKey, publicKeyFileName, username, ip, port, key string) error {
session, err := createSSHPublicKeysSession(username, ip, port, key)
if err != nil {
return err
@ -32,12 +31,12 @@ func uploadPublicKeyScp(publicKey, username, ip, port, key string) error {
defer session.Close()
go func() {
w, _ := session.StdinPipe()
fmt.Fprintln(w, "C0644", len(publicKey), dynamicPublicKeyFileName)
fmt.Fprintln(w, "C0644", len(publicKey), publicKeyFileName)
io.Copy(w, strings.NewReader(publicKey))
fmt.Fprint(w, "\x00")
w.Close()
}()
err = session.Run(fmt.Sprintf("scp -vt %s", dynamicPublicKeyFileName))
err = session.Run(fmt.Sprintf("scp -vt %s", publicKeyFileName))
if err != nil {
return fmt.Errorf("public key upload failed")
}
@ -87,8 +86,8 @@ func createSSHPublicKeysSession(username, ipAddr, port, hostKey string) (*ssh.Se
// Creates a new RSA key pair with key length of 2048.
// The private key will be of pem format and the public key will be
// of OpenSSH format.
func generateRSAKeys() (publicKeyRsa string, privateKeyRsa string, err error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
func generateRSAKeys(keyBits int) (publicKeyRsa string, privateKeyRsa string, err error) {
privateKey, err := rsa.GenerateKey(rand.Reader, keyBits)
if err != nil {
return "", "", fmt.Errorf("error generating RSA key-pair: %s", err)
}
@ -108,7 +107,7 @@ func generateRSAKeys() (publicKeyRsa string, privateKeyRsa string, err error) {
// Concatenates the public present in that target machine's home
// folder to ~/.ssh/authorized_keys file
func installPublicKeyInTarget(adminUser, username, ip, port, hostKey string) error {
func installPublicKeyInTarget(adminUser, publicKeyFileName, username, ip, port, hostKey string) error {
session, err := createSSHPublicKeysSession(adminUser, ip, port, hostKey)
if err != nil {
return fmt.Errorf("unable to create SSH Session using public keys: %s", err)
@ -122,11 +121,10 @@ func installPublicKeyInTarget(adminUser, username, ip, port, hostKey string) err
tempKeysFileName := fmt.Sprintf("/home/%s/temp_authorized_keys", username)
// Commands to be run on target machine
dynamicPublicKeyFileName := fmt.Sprintf("vault_ssh_%s_%s.pub", username, ip)
grepCmd := fmt.Sprintf("grep -vFf %s %s > %s", dynamicPublicKeyFileName, authKeysFileName, tempKeysFileName)
grepCmd := fmt.Sprintf("grep -vFf %s %s > %s", publicKeyFileName, authKeysFileName, tempKeysFileName)
catCmdRemoveDuplicate := fmt.Sprintf("cat %s > %s", tempKeysFileName, authKeysFileName)
catCmdAppendNew := fmt.Sprintf("cat %s >> %s", dynamicPublicKeyFileName, authKeysFileName)
removeCmd := fmt.Sprintf("rm -f %s %s", tempKeysFileName, dynamicPublicKeyFileName)
catCmdAppendNew := fmt.Sprintf("cat %s >> %s", publicKeyFileName, authKeysFileName)
removeCmd := fmt.Sprintf("rm -f %s %s", tempKeysFileName, publicKeyFileName)
targetCmd := fmt.Sprintf("%s;%s;%s;%s", grepCmd, catCmdRemoveDuplicate, catCmdAppendNew, removeCmd)
session.Run(targetCmd)
@ -135,7 +133,7 @@ func installPublicKeyInTarget(adminUser, username, ip, port, hostKey string) err
// Removes the installed public key from the authorized_keys file
// in target machine
func uninstallPublicKeyInTarget(adminUser, username, ip, port, hostKey string) error {
func uninstallPublicKeyInTarget(adminUser, publicKeyFileName, username, ip, port, hostKey string) error {
session, err := createSSHPublicKeysSession(adminUser, ip, port, hostKey)
if err != nil {
return fmt.Errorf("unable to create SSH Session using public keys: %s", err)
@ -149,10 +147,9 @@ func uninstallPublicKeyInTarget(adminUser, username, ip, port, hostKey string) e
tempKeysFileName := fmt.Sprintf("/home/%s/temp_authorized_keys", username)
// Commands to be run on target machine
dynamicPublicKeyFileName := fmt.Sprintf("vault_ssh_%s_%s.pub", username, ip)
grepCmd := fmt.Sprintf("grep -vFf %s %s > %s", dynamicPublicKeyFileName, authKeysFileName, tempKeysFileName)
grepCmd := fmt.Sprintf("grep -vFf %s %s > %s", publicKeyFileName, authKeysFileName, tempKeysFileName)
catCmdRemoveDuplicate := fmt.Sprintf("cat %s > %s", tempKeysFileName, authKeysFileName)
removeCmd := fmt.Sprintf("rm -f %s %s", tempKeysFileName, dynamicPublicKeyFileName)
removeCmd := fmt.Sprintf("rm -f %s %s", tempKeysFileName, publicKeyFileName)
remoteCmd := fmt.Sprintf("%s;%s;%s", grepCmd, catCmdRemoveDuplicate, removeCmd)
session.Run(remoteCmd)

View File

@ -6,9 +6,9 @@ import (
"net"
"os"
"os/exec"
"os/user"
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/logical/ssh"
)
@ -19,13 +19,16 @@ type SSHCommand struct {
}
func (c *SSHCommand) Run(args []string) int {
var role string
var port string
var role, port, path string
var noExec bool
var sshCmdArgs []string
var sshDynamicKeyFileName string
flags := c.Meta.FlagSet("ssh", FlagSetDefault)
flags.StringVar(&role, "role", "", "")
flags.StringVar(&port, "port", "22", "")
flags.StringVar(&path, "path", "ssh", "")
flags.BoolVar(&noExec, "no-exec", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
@ -43,23 +46,33 @@ func (c *SSHCommand) Run(args []string) int {
}
input := strings.Split(args[0], "@")
if len(input) != 2 {
var username string
var ipAddr string
if len(input) == 1 {
u, err := user.Current()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error fetching username: '%s'", err))
}
username = u.Username
ipAddr = input[0]
} else if len(input) == 2 {
username = input[0]
ipAddr = input[1]
} else {
c.Ui.Error(fmt.Sprintf("Invalid parameter: %s", args[0]))
return 2
}
username := input[0]
ip, err := net.ResolveIPAddr("ip", input[1])
ip, err := net.ResolveIPAddr("ip", ipAddr)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error resolving IP Address: %s", err))
return 2
}
if role == "" {
role, err = setDefaultRole(client, ip.String())
role, err = c.defaultRole(path, ip.String())
if err != nil {
c.Ui.Error(fmt.Sprintf("Error setting default role: %s", err.Error()))
c.Ui.Error(fmt.Sprintf("Error setting default role: '%s'", err))
return 1
}
c.Ui.Output(fmt.Sprintf("Vault SSH: Role:'%s'\n", role))
@ -70,12 +83,17 @@ func (c *SSHCommand) Run(args []string) int {
"ip": ip.String(),
}
keySecret, err := client.SSH().KeyCreate(role, data)
keySecret, err := client.SSH(path).Credential(role, data)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting key for SSH session:%s", err))
return 2
}
if noExec {
c.Ui.Output(fmt.Sprintf("IP:%s\nUsername: %s\nKey:%s\n", ip.String(), username, keySecret.Data["key"]))
return 0
}
if keySecret.Data["key_type"].(string) == ssh.KeyTypeDynamic {
sshDynamicKey := string(keySecret.Data["key"].(string))
if len(sshDynamicKey) == 0 {
@ -87,9 +105,8 @@ func (c *SSHCommand) Run(args []string) int {
sshCmdArgs = append(sshCmdArgs, []string{"-i", sshDynamicKeyFileName}...)
} else if keySecret.Data["key_type"].(string) == ssh.KeyTypeOTP {
fmt.Printf("OTP for the session is %s\n", string(keySecret.Data["key"].(string)))
c.Ui.Output(fmt.Sprintf("OTP for the session is %s\n", string(keySecret.Data["key"].(string))))
} else {
// Intentionally not mentioning the exact error
c.Ui.Error("Error creating key")
}
sshCmdArgs = append(sshCmdArgs, []string{"-p", port}...)
@ -107,25 +124,30 @@ func (c *SSHCommand) Run(args []string) int {
if keySecret.Data["key_type"].(string) == ssh.KeyTypeDynamic {
err = os.Remove(sshDynamicKeyFileName)
if err != nil {
// Intentionally not mentioning the exact error
c.Ui.Error("Error cleaning up")
c.Ui.Error(fmt.Sprintf("Error deleting key file: %s", err))
}
}
err = client.SSH().KeyRevoke(keySecret.LeaseID)
err = client.Sys().Revoke(keySecret.LeaseID)
if err != nil {
// Intentionally not mentioning the exact error
c.Ui.Error("Error cleaning up")
c.Ui.Error(fmt.Sprintf("Error revoking the key: %s", err))
}
return 0
}
func setDefaultRole(client *api.Client, ip string) (string, error) {
// If user did not provide the role with which SSH connection has
// to be established and if there is only one role associated with
// the IP, it is used by default.
func (c *SSHCommand) defaultRole(path, ip string) (string, error) {
data := map[string]interface{}{
"ip": ip,
}
secret, err := client.Logical().Write("ssh/lookup", data)
client, err := c.Client()
if err != nil {
return "", err
}
secret, err := client.Logical().Write(path+"/lookup", data)
if err != nil {
return "", fmt.Errorf("Error finding roles for IP '%s':%s", ip, err)
@ -135,13 +157,18 @@ func setDefaultRole(client *api.Client, ip string) (string, error) {
}
if secret.Data["roles"] == nil {
return "", fmt.Errorf("IP '%s' not registered under any role", ip)
return "", fmt.Errorf("No matching roles found for IP '%s'", ip)
}
if len(secret.Data["roles"].([]interface{})) == 1 {
return secret.Data["roles"].([]interface{})[0].(string), nil
} else {
return "", fmt.Errorf("Multiple roles for IP '%s'. Select one of '%s' using '-role' option", ip, secret.Data["roles"])
var roleNames string
for _, item := range secret.Data["roles"].([]interface{}) {
roleNames += item.(string) + ", "
}
roleNames = strings.TrimRight(roleNames, ", ")
return "", fmt.Errorf("IP '%s' has multiple roles.\nSelect a role using '-role' option.\nPossible roles: [%s]\nNote that all roles may not be permitted, based on ACLs.", ip, roleNames)
}
}
@ -160,21 +187,31 @@ Usage: vault ssh [options] username@ip
that SSH backend is mounted and at least one 'role' be registed
with vault at priori.
For setting up SSH backends with one-time-passwords, installation
of agent in target machines is required.
See [https://github.com/hashicorp/vault-ssh-agent]
General Options:
` + generalOptionsUsage() + `
SSH Options:
-role Mention the role to be used to create dynamic key.
-role Role to be used to create the key.
Each IP is associated with a role. To see the associated
roles with IP, use "lookup" endpoint. If you are certain that
there is only one role associated with the IP, you can
skip mentioning the role. It will be chosen by default.
If there are no roless 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.
-port Port number to use for SSH connection. This defaults to port 22.
-no-exec Shows the credentials but does not establish connection.
-path Mount point of SSH backend. If the backend is mounted at
'ssh', which is the default as well, this parameter can
be skipped.
`
return strings.TrimSpace(helpText)
}