2015-06-26 18:08:03 +00:00
|
|
|
package ssh
|
|
|
|
|
|
|
|
import (
|
2015-08-06 18:48:19 +00:00
|
|
|
"bytes"
|
2015-06-26 18:08:03 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/pem"
|
|
|
|
"fmt"
|
2015-07-02 01:26:42 +00:00
|
|
|
"net"
|
|
|
|
"strings"
|
2015-08-06 18:48:19 +00:00
|
|
|
"time"
|
2015-07-02 01:26:42 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/vault/logical"
|
2015-06-26 18:08:03 +00:00
|
|
|
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
)
|
|
|
|
|
2015-08-18 01:22:03 +00:00
|
|
|
// Creates a new RSA key pair with the given key length. The private key will be
|
|
|
|
// of pem format and the public key will be of OpenSSH format.
|
2015-07-29 18:21:36 +00:00
|
|
|
func generateRSAKeys(keyBits int) (publicKeyRsa string, privateKeyRsa string, err error) {
|
|
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, keyBits)
|
2015-06-26 18:08:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", "", fmt.Errorf("error generating RSA key-pair: %s", err)
|
|
|
|
}
|
|
|
|
|
2015-06-30 20:30:13 +00:00
|
|
|
privateKeyRsa = string(pem.EncodeToMemory(&pem.Block{
|
2015-06-26 18:08:03 +00:00
|
|
|
Type: "RSA PRIVATE KEY",
|
|
|
|
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
|
|
}))
|
|
|
|
|
|
|
|
sshPublicKey, err := ssh.NewPublicKey(privateKey.Public())
|
|
|
|
if err != nil {
|
|
|
|
return "", "", fmt.Errorf("error generating RSA key-pair: %s", err)
|
|
|
|
}
|
2015-06-30 20:30:13 +00:00
|
|
|
publicKeyRsa = "ssh-rsa " + base64.StdEncoding.EncodeToString(sshPublicKey.Marshal())
|
|
|
|
return
|
2015-06-26 18:08:03 +00:00
|
|
|
}
|
2015-07-02 01:26:42 +00:00
|
|
|
|
2015-08-18 01:22:03 +00:00
|
|
|
// Public key and the script to install the key are uploaded to remote machine.
|
|
|
|
// Public key is either added or removed from authorized_keys file using the
|
|
|
|
// script. Default script is for a Linux machine and hence the path of the
|
|
|
|
// authorized_keys file is hard coded to resemble Linux.
|
|
|
|
//
|
|
|
|
// The last param 'install' if false, uninstalls the key.
|
|
|
|
func (b *backend) installPublicKeyInTarget(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.
|
2016-01-13 18:40:08 +00:00
|
|
|
_, publicKeyFileName, err := b.GenerateSaltedOTP()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-01-19 06:59:08 +00:00
|
|
|
comm, err := createSSHComm(adminUser, ip, port, hostkey)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer comm.Close()
|
|
|
|
|
|
|
|
err = comm.Upload(publicKeyFileName, bytes.NewBufferString(dynamicPublicKey), nil)
|
2015-08-18 01:22:03 +00:00
|
|
|
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)
|
2016-01-19 06:59:08 +00:00
|
|
|
err = comm.Upload(scriptFileName, bytes.NewBufferString(installScript), nil)
|
2015-08-18 01:22:03 +00:00
|
|
|
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.
|
2016-01-19 06:59:08 +00:00
|
|
|
session, err := comm.NewSession()
|
2015-07-02 21:23:09 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to create SSH Session using public keys: %s", err)
|
|
|
|
}
|
|
|
|
if session == nil {
|
|
|
|
return fmt.Errorf("invalid session object")
|
|
|
|
}
|
|
|
|
defer session.Close()
|
|
|
|
|
2015-07-10 22:18:02 +00:00
|
|
|
authKeysFileName := fmt.Sprintf("/home/%s/.ssh/authorized_keys", username)
|
2015-07-02 21:23:09 +00:00
|
|
|
|
2015-08-13 17:36:31 +00:00
|
|
|
var installOption string
|
|
|
|
if install {
|
|
|
|
installOption = "install"
|
|
|
|
} else {
|
|
|
|
installOption = "uninstall"
|
2015-07-02 21:23:09 +00:00
|
|
|
}
|
2015-08-18 01:22:03 +00:00
|
|
|
|
2015-08-06 19:50:12 +00:00
|
|
|
// Give execute permissions to install script, run and delete it.
|
|
|
|
chmodCmd := fmt.Sprintf("chmod +x %s", scriptFileName)
|
2015-08-13 17:36:31 +00:00
|
|
|
scriptCmd := fmt.Sprintf("./%s %s %s %s", scriptFileName, installOption, publicKeyFileName, authKeysFileName)
|
2015-08-06 19:50:12 +00:00
|
|
|
rmCmd := fmt.Sprintf("rm -f %s", scriptFileName)
|
|
|
|
targetCmd := fmt.Sprintf("%s;%s;%s", chmodCmd, scriptCmd, rmCmd)
|
|
|
|
|
|
|
|
session.Run(targetCmd)
|
2015-07-02 21:23:09 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-07-27 20:42:03 +00:00
|
|
|
// Takes an IP address and role name and checks if the IP is part
|
|
|
|
// of CIDR blocks belonging to the role.
|
2015-07-02 21:23:09 +00:00
|
|
|
func roleContainsIP(s logical.Storage, roleName string, ip string) (bool, error) {
|
2015-07-02 01:26:42 +00:00
|
|
|
if roleName == "" {
|
|
|
|
return false, fmt.Errorf("missing role name")
|
|
|
|
}
|
2015-07-27 20:42:03 +00:00
|
|
|
|
2015-07-02 01:26:42 +00:00
|
|
|
if ip == "" {
|
|
|
|
return false, fmt.Errorf("missing ip")
|
|
|
|
}
|
2015-07-27 20:42:03 +00:00
|
|
|
|
2015-08-13 00:36:27 +00:00
|
|
|
roleEntry, err := s.Get(fmt.Sprintf("roles/%s", roleName))
|
2015-07-02 01:26:42 +00:00
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("error retrieving role '%s'", err)
|
|
|
|
}
|
|
|
|
if roleEntry == nil {
|
|
|
|
return false, fmt.Errorf("role '%s' not found", roleName)
|
|
|
|
}
|
2015-07-27 20:42:03 +00:00
|
|
|
|
2015-07-02 01:26:42 +00:00
|
|
|
var role sshRole
|
|
|
|
if err := roleEntry.DecodeJSON(&role); err != nil {
|
|
|
|
return false, fmt.Errorf("error decoding role '%s'", roleName)
|
|
|
|
}
|
2015-07-02 21:23:09 +00:00
|
|
|
|
2015-08-28 03:19:55 +00:00
|
|
|
if matched, err := cidrListContainsIP(ip, role.CIDRList); err != nil {
|
2015-07-02 21:23:09 +00:00
|
|
|
return false, err
|
|
|
|
} else {
|
|
|
|
return matched, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-29 19:24:15 +00:00
|
|
|
// Checks if the comma separated list of CIDR blocks are all valid and they
|
|
|
|
// dont conflict with each other.
|
|
|
|
func validateCIDRList(cidrList string) (string, error) {
|
|
|
|
// Check if the blocks are parsable
|
|
|
|
c := strings.Split(cidrList, ",")
|
|
|
|
for _, item := range c {
|
2015-08-18 01:22:03 +00:00
|
|
|
_, _, err := net.ParseCIDR(item)
|
|
|
|
if err != nil {
|
2015-08-29 19:24:15 +00:00
|
|
|
return "", err
|
2015-08-18 01:22:03 +00:00
|
|
|
}
|
|
|
|
}
|
2015-08-29 19:24:15 +00:00
|
|
|
|
|
|
|
var overlaps string
|
|
|
|
for i := 0; i < len(c)-1; i++ {
|
|
|
|
for j := i + 1; j < len(c); j++ {
|
|
|
|
overlap, err := cidrOverlap(c[i], c[j])
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if overlap {
|
|
|
|
overlaps = fmt.Sprintf("%s [%s,%s]", overlaps, c[i], c[j])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return overlaps, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tells if the CIDR blocks overlap with eath other. Applying the mask of bigger
|
|
|
|
// block to both addresses and checking for its equality to detect an overlap.
|
|
|
|
func cidrOverlap(c1, c2 string) (bool, error) {
|
|
|
|
ip1, net1, err := net.ParseCIDR(c1)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
maskLen1, _ := net1.Mask.Size()
|
|
|
|
|
|
|
|
ip2, net2, err := net.ParseCIDR(c2)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
maskLen2, _ := net2.Mask.Size()
|
|
|
|
|
|
|
|
// Choose the mask of bigger block
|
|
|
|
mask := net1.Mask
|
|
|
|
if maskLen2 < maskLen1 {
|
|
|
|
mask = net2.Mask
|
|
|
|
}
|
|
|
|
|
|
|
|
return bytes.Equal(ip1.Mask(mask), ip2.Mask(mask)), nil
|
2015-08-18 01:22:03 +00:00
|
|
|
}
|
|
|
|
|
2015-07-27 20:42:03 +00:00
|
|
|
// Returns true if the IP supplied by the user is part of the comma
|
|
|
|
// separated CIDR blocks
|
2015-08-28 03:19:55 +00:00
|
|
|
func cidrListContainsIP(ip, cidrList string) (bool, error) {
|
2015-08-29 19:24:15 +00:00
|
|
|
if len(cidrList) == 0 {
|
|
|
|
return false, fmt.Errorf("IP does not belong to role")
|
|
|
|
}
|
2015-08-13 15:46:55 +00:00
|
|
|
for _, item := range strings.Split(cidrList, ",") {
|
2015-07-02 01:26:42 +00:00
|
|
|
_, cidrIPNet, err := net.ParseCIDR(item)
|
|
|
|
if err != nil {
|
2015-08-13 15:46:55 +00:00
|
|
|
return false, fmt.Errorf("invalid CIDR entry '%s'", item)
|
2015-07-02 01:26:42 +00:00
|
|
|
}
|
2015-07-02 21:23:09 +00:00
|
|
|
if cidrIPNet.Contains(net.ParseIP(ip)) {
|
|
|
|
return true, nil
|
2015-07-02 01:26:42 +00:00
|
|
|
}
|
|
|
|
}
|
2015-07-02 21:23:09 +00:00
|
|
|
return false, nil
|
2015-07-02 01:26:42 +00:00
|
|
|
}
|
2015-08-06 18:48:19 +00:00
|
|
|
|
2016-01-19 06:59:08 +00:00
|
|
|
func createSSHComm(username, ip string, port int, hostkey string) (*comm, error) {
|
2015-08-06 18:48:19 +00:00
|
|
|
signer, err := ssh.ParsePrivateKey([]byte(hostkey))
|
2016-01-19 06:59:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2015-08-06 18:48:19 +00:00
|
|
|
clientConfig := &ssh.ClientConfig{
|
|
|
|
User: username,
|
|
|
|
Auth: []ssh.AuthMethod{
|
|
|
|
ssh.PublicKeys(signer),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
connfunc := func() (net.Conn, error) {
|
2015-08-13 15:46:55 +00:00
|
|
|
c, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), 15*time.Second)
|
2015-08-06 18:48:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if tcpConn, ok := c.(*net.TCPConn); ok {
|
|
|
|
tcpConn.SetKeepAlive(true)
|
|
|
|
tcpConn.SetKeepAlivePeriod(5 * time.Second)
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|
2015-08-13 15:46:55 +00:00
|
|
|
config := &SSHCommConfig{
|
2015-08-06 18:48:19 +00:00
|
|
|
SSHConfig: clientConfig,
|
|
|
|
Connection: connfunc,
|
|
|
|
Pty: false,
|
|
|
|
DisableAgent: true,
|
|
|
|
}
|
2016-01-19 06:59:08 +00:00
|
|
|
|
|
|
|
return SSHCommNew(fmt.Sprintf("%s:%d", ip, port), config)
|
2015-08-06 18:48:19 +00:00
|
|
|
}
|