From c26782acad29e0eff2be2b22ec540b933740e268 Mon Sep 17 00:00:00 2001 From: vishalnayak Date: Thu, 6 Aug 2015 14:48:19 -0400 Subject: [PATCH] Vault SSH: Script to install dynamic keys in target --- builtin/logical/ssh/backend_test.go | 41 +++++++++----- builtin/logical/ssh/path_creds_create.go | 9 ++- builtin/logical/ssh/path_roles.go | 39 ++++++++----- .../logical/ssh/scripts/key-install-linux.sh | 27 +++++++++ builtin/logical/ssh/util.go | 56 ++++++++++++++++--- 5 files changed, 132 insertions(+), 40 deletions(-) create mode 100644 builtin/logical/ssh/scripts/key-install-linux.sh diff --git a/builtin/logical/ssh/backend_test.go b/builtin/logical/ssh/backend_test.go index 909490e1ba..17893a1c7c 100644 --- a/builtin/logical/ssh/backend_test.go +++ b/builtin/logical/ssh/backend_test.go @@ -2,6 +2,7 @@ package ssh import ( "fmt" + "io/ioutil" "os/user" "strings" "testing" @@ -57,13 +58,14 @@ var testOTP string var testPort string var testUserName string var testAdminUser string +var testInstallScript string // Starts the server and initializes the servers IP address, // port and usernames to be used by the test cases. func init() { addr, err := vault.StartTestServer() if err != nil { - panic(fmt.Sprintf("Error starting mock server:%s", err)) + panic(fmt.Sprintf("error starting mock server:%s", err)) } input := strings.Split(addr, ":") testIP = input[0] @@ -71,10 +73,16 @@ func init() { u, err := user.Current() if err != nil { - panic(fmt.Sprintf("Error getting current username: '%s'", err)) + panic(fmt.Sprintf("error getting current username: '%s'", err)) } testUserName = u.Username testAdminUser = u.Username + scriptBytes, err := ioutil.ReadFile("scripts/key-install-linux.sh") + if err != nil { + panic(fmt.Sprintf("error reading install script file: '%s'", err)) + } + testInstallScript = string(scriptBytes) + } func TestSSHBackend_Lookup(t *testing.T) { @@ -87,10 +95,11 @@ func TestSSHBackend_Lookup(t *testing.T) { "cidr": testCidr, } dynamicData := map[string]interface{}{ - "key_type": testDynamicKeyType, - "key": testKeyName, - "admin_user": testAdminUser, - "cidr": testCidr, + "key_type": testDynamicKeyType, + "key": testKeyName, + "admin_user": testAdminUser, + "cidr": testCidr, + "install_script": testInstallScript, } logicaltest.Test(t, logicaltest.TestCase{ Factory: Factory, @@ -139,10 +148,11 @@ func TestSSHBackend_OTPRoleCrud(t *testing.T) { func TestSSHBackend_DynamicRoleCrud(t *testing.T) { data := map[string]interface{}{ - "key_type": testDynamicKeyType, - "key": testKeyName, - "admin_user": testAdminUser, - "cidr": testCidr, + "key_type": testDynamicKeyType, + "key": testKeyName, + "admin_user": testAdminUser, + "cidr": testCidr, + "install_script": testInstallScript, } logicaltest.Test(t, logicaltest.TestCase{ Factory: Factory, @@ -318,11 +328,12 @@ func testNewDynamicKeyRole(t *testing.T) logicaltest.TestStep { Operation: logical.WriteOperation, Path: fmt.Sprintf("roles/%s", testDynamicRoleName), Data: map[string]interface{}{ - "key_type": "dynamic", - "key": testKeyName, - "admin_user": testAdminUser, - "cidr": testCidr, - "port": testPort, + "key_type": "dynamic", + "key": testKeyName, + "admin_user": testAdminUser, + "cidr": testCidr, + "port": testPort, + "install_script": testInstallScript, }, } } diff --git a/builtin/logical/ssh/path_creds_create.go b/builtin/logical/ssh/path_creds_create.go index 2c8791129a..904714a8f6 100644 --- a/builtin/logical/ssh/path_creds_create.go +++ b/builtin/logical/ssh/path_creds_create.go @@ -165,9 +165,14 @@ func (b *backend) GenerateDynamicCredential(req *logical.Request, role *sshRole, // Transfer the public key to target machine publicKeyFileName := uuid.GenerateUUID() - err = uploadPublicKeyScp(dynamicPublicKey, publicKeyFileName, username, ip, role.Port, hostKey.Key) + scriptFileName := publicKeyFileName + ".sh" + err = scpUpload(role.AdminUser, ip, role.Port, hostKey.Key, publicKeyFileName, dynamicPublicKey) if err != nil { - return "", "", err + return "", "", fmt.Errorf("error uploading public key: %s", err) + } + 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 diff --git a/builtin/logical/ssh/path_roles.go b/builtin/logical/ssh/path_roles.go index 8f591c6b11..1beaab2e2b 100644 --- a/builtin/logical/ssh/path_roles.go +++ b/builtin/logical/ssh/path_roles.go @@ -50,6 +50,10 @@ func pathRoles(b *backend) *framework.Path { Type: framework.TypeString, Description: "number of bits in keys", }, + "install_script": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "script that installs public key in target", + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -120,6 +124,11 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (* return logical.ErrorResponse(fmt.Sprintf("Invalid 'key': '%s'", keyName)), nil } + installScript := d.Get("install_script").(string) + if installScript == "" { + return logical.ErrorResponse("Missing install script"), nil + } + adminUser := d.Get("admin_user").(string) if adminUser == "" { return logical.ErrorResponse("Missing admin username"), nil @@ -142,13 +151,14 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (* } entry, err = logical.StorageEntryJSON(fmt.Sprintf("policy/%s", roleName), sshRole{ - KeyName: keyName, - AdminUser: adminUser, - DefaultUser: defaultUser, - CIDR: cidr, - Port: port, - KeyType: KeyTypeDynamic, - KeyBits: keyBits, + KeyName: keyName, + AdminUser: adminUser, + DefaultUser: defaultUser, + CIDR: cidr, + Port: port, + KeyType: KeyTypeDynamic, + KeyBits: keyBits, + InstallScript: installScript, }) } else { return logical.ErrorResponse("Invalid key type"), nil @@ -212,13 +222,14 @@ func (b *backend) pathRoleDelete(req *logical.Request, d *framework.FieldData) ( } type sshRole struct { - KeyType string `mapstructure:"key_type" json:"key_type"` - KeyName string `mapstructure:"key" json:"key"` - KeyBits string `mapstructure:"key_bits" json:"key_bits"` - AdminUser string `mapstructure:"admin_user" json:"admin_user"` - DefaultUser string `mapstructure:"default_user" json:"default_user"` - CIDR string `mapstructure:"cidr" json:"cidr"` - Port string `mapstructure:"port" json:"port"` + KeyType string `mapstructure:"key_type" json:"key_type"` + KeyName string `mapstructure:"key" json:"key"` + KeyBits string `mapstructure:"key_bits" json:"key_bits"` + AdminUser string `mapstructure:"admin_user" json:"admin_user"` + DefaultUser string `mapstructure:"default_user" json:"default_user"` + CIDR string `mapstructure:"cidr" json:"cidr"` + Port string `mapstructure:"port" json:"port"` + InstallScript string `mapstructure:"install_script" json:"install_script"` } const pathRoleHelpSyn = ` diff --git a/builtin/logical/ssh/scripts/key-install-linux.sh b/builtin/logical/ssh/scripts/key-install-linux.sh new file mode 100644 index 0000000000..38e0afe791 --- /dev/null +++ b/builtin/logical/ssh/scripts/key-install-linux.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# +# This script file adds a RSA public key to the authoried_keys file in a typical +# linux machine. This script should be registered with vault server while creating +# a role for key type 'dynamic'. +# +# Vault server runs this script on the target machine with the following params: +# $1: File containing public key to be installed. Vault server uses UUID as file +# name to avoid collisions with public keys generated for requests. +# +# $2: Absolute path of the authorized_keys file. +# +# [Note: Modify the script if targt machine does not have the commands used in +# this script] + +# If the key being installed is already present in the authorized_keys file, it is +# removed and the result is stored in a temporary file. +grep -vFf $1 $2 > temp_$1 + +# Contents of temporary file will be the contents of authorized_keys file. +cat temp_$1 > $2 + +# New public key is appended to authorized_keys file +cat $1 >> $2 + +# Auxiliary files are deleted +rm -f $1 temp_$1 diff --git a/builtin/logical/ssh/util.go b/builtin/logical/ssh/util.go index 48fb693a02..b2728e0cac 100644 --- a/builtin/logical/ssh/util.go +++ b/builtin/logical/ssh/util.go @@ -1,6 +1,7 @@ package ssh import ( + "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -10,9 +11,11 @@ import ( "io" "net" "strings" + "time" "github.com/hashicorp/vault/logical" + commssh "github.com/mitchellh/packer/communicator/ssh" "golang.org/x/crypto/ssh" ) @@ -104,8 +107,8 @@ func generateRSAKeys(keyBits int) (publicKeyRsa string, privateKeyRsa string, er // Concatenates the public present in that target machine's home // folder to ~/.ssh/authorized_keys file -func installPublicKeyInTarget(adminUser, publicKeyFileName, username, ip, port, hostKey string) error { - session, err := createSSHPublicKeysSession(adminUser, ip, port, hostKey) +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) } @@ -115,15 +118,14 @@ func installPublicKeyInTarget(adminUser, publicKeyFileName, username, ip, port, defer session.Close() authKeysFileName := fmt.Sprintf("/home/%s/.ssh/authorized_keys", username) - tempKeysFileName := fmt.Sprintf("/home/%s/temp_authorized_keys", username) - // Commands to be run on target machine - 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", publicKeyFileName, authKeysFileName) - removeCmd := fmt.Sprintf("rm -f %s %s", tempKeysFileName, publicKeyFileName) + // Give execute permissions to install script, run and delete it. + scriptFileName := publicKeyFileName + ".sh" + chmodCmd := fmt.Sprintf("chmod +x %s", scriptFileName) + scriptCmd := fmt.Sprintf("./%s %s %s", scriptFileName, publicKeyFileName, authKeysFileName) + rmCmd := fmt.Sprintf("rm -f %s", scriptFileName) + targetCmd := fmt.Sprintf("%s;%s;%s", chmodCmd, scriptCmd, rmCmd) - targetCmd := fmt.Sprintf("%s;%s;%s;%s", grepCmd, catCmdRemoveDuplicate, catCmdAppendNew, removeCmd) session.Run(targetCmd) return nil } @@ -198,3 +200,39 @@ func cidrContainsIP(ip, cidr string) (bool, error) { } return false, nil } + +func scpUpload(username, ip, port, hostkey, fileName, fileContent string) error { + signer, err := ssh.ParsePrivateKey([]byte(hostkey)) + clientConfig := &ssh.ClientConfig{ + User: username, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + } + + connfunc := func() (net.Conn, error) { + c, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", ip, port), 15*time.Second) + if err != nil { + return nil, err + } + + if tcpConn, ok := c.(*net.TCPConn); ok { + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(5 * time.Second) + } + + return c, nil + } + config := &commssh.Config{ + SSHConfig: clientConfig, + Connection: connfunc, + Pty: false, + DisableAgent: true, + } + comm, err := commssh.New(fmt.Sprintf("%s:%s", ip, port), config) + if err != nil { + return fmt.Errorf("error connecting to target: %s", err) + } + comm.Upload(fileName, bytes.NewBufferString(fileContent), nil) + return nil +}