2015-06-16 20:58:54 +00:00
|
|
|
package ssh
|
|
|
|
|
|
|
|
import (
|
2015-06-30 20:30:13 +00:00
|
|
|
"fmt"
|
|
|
|
"net"
|
2015-07-29 18:21:36 +00:00
|
|
|
"strconv"
|
2015-06-30 20:30:13 +00:00
|
|
|
"strings"
|
2015-06-16 20:58:54 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/vault/logical"
|
|
|
|
"github.com/hashicorp/vault/logical/framework"
|
|
|
|
)
|
|
|
|
|
2015-07-29 18:21:36 +00:00
|
|
|
const KeyTypeOTP = "otp"
|
|
|
|
const KeyTypeDynamic = "dynamic"
|
|
|
|
const KeyBitsRSA = "2048"
|
|
|
|
|
2015-06-16 20:58:54 +00:00
|
|
|
func pathRoles(b *backend) *framework.Path {
|
|
|
|
return &framework.Path{
|
2015-08-13 00:36:27 +00:00
|
|
|
Pattern: "roles/(?P<role>[-\\w]+)",
|
2015-06-16 20:58:54 +00:00
|
|
|
Fields: map[string]*framework.FieldSchema{
|
2015-08-13 00:36:27 +00:00
|
|
|
"role": &framework.FieldSchema{
|
2015-06-16 20:58:54 +00:00
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "Name of the role",
|
|
|
|
},
|
2015-06-24 22:13:12 +00:00
|
|
|
"key": &framework.FieldSchema{
|
2015-06-16 20:58:54 +00:00
|
|
|
Type: framework.TypeString,
|
2015-06-24 22:13:12 +00:00
|
|
|
Description: "Named key in Vault",
|
|
|
|
},
|
|
|
|
"admin_user": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "Admin user at target address",
|
|
|
|
},
|
|
|
|
"default_user": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "Default user to whom the dynamic key is installed",
|
|
|
|
},
|
|
|
|
"cidr": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "CIDR blocks and IP addresses",
|
2015-06-16 20:58:54 +00:00
|
|
|
},
|
2015-07-06 20:56:45 +00:00
|
|
|
"port": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "Port number for SSH connection",
|
|
|
|
},
|
2015-07-22 18:15:19 +00:00
|
|
|
"key_type": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "one-time-password or dynamic-key",
|
|
|
|
},
|
2015-07-29 18:21:36 +00:00
|
|
|
"key_bits": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "number of bits in keys",
|
|
|
|
},
|
2015-08-06 18:48:19 +00:00
|
|
|
"install_script": &framework.FieldSchema{
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "script that installs public key in target",
|
|
|
|
},
|
2015-06-16 20:58:54 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
|
|
logical.ReadOperation: b.pathRoleRead,
|
|
|
|
logical.WriteOperation: b.pathRoleWrite,
|
|
|
|
logical.DeleteOperation: b.pathRoleDelete,
|
|
|
|
},
|
|
|
|
|
|
|
|
HelpSynopsis: pathRoleHelpSyn,
|
|
|
|
HelpDescription: pathRoleHelpDesc,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-29 18:21:36 +00:00
|
|
|
func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
2015-08-13 00:36:27 +00:00
|
|
|
roleName := d.Get("role").(string)
|
2015-07-22 18:15:19 +00:00
|
|
|
if roleName == "" {
|
|
|
|
return logical.ErrorResponse("Missing role name"), nil
|
|
|
|
}
|
2015-07-27 17:02:31 +00:00
|
|
|
|
|
|
|
cidr := d.Get("cidr").(string)
|
2015-07-22 18:15:19 +00:00
|
|
|
if cidr == "" {
|
2015-08-13 00:36:27 +00:00
|
|
|
return logical.ErrorResponse("Missing CIDR blocks"), nil
|
2015-07-22 18:15:19 +00:00
|
|
|
}
|
|
|
|
for _, item := range strings.Split(cidr, ",") {
|
|
|
|
_, _, err := net.ParseCIDR(item)
|
|
|
|
if err != nil {
|
|
|
|
return logical.ErrorResponse(fmt.Sprintf("Invalid cidr entry '%s'", item)), nil
|
|
|
|
}
|
|
|
|
}
|
2015-07-27 17:02:31 +00:00
|
|
|
|
|
|
|
port := d.Get("port").(string)
|
2015-07-22 18:15:19 +00:00
|
|
|
if port == "" {
|
|
|
|
port = "22"
|
|
|
|
}
|
2015-07-27 17:02:31 +00:00
|
|
|
|
2015-07-29 18:21:36 +00:00
|
|
|
keyType := d.Get("key_type").(string)
|
|
|
|
if keyType == "" {
|
|
|
|
return logical.ErrorResponse("Missing key type"), nil
|
2015-07-22 18:15:19 +00:00
|
|
|
}
|
2015-07-29 18:21:36 +00:00
|
|
|
keyType = strings.ToLower(keyType)
|
2015-07-22 18:15:19 +00:00
|
|
|
|
2015-07-29 18:21:36 +00:00
|
|
|
var entry *logical.StorageEntry
|
|
|
|
var err error
|
|
|
|
if keyType == KeyTypeOTP {
|
|
|
|
adminUser := d.Get("admin_user").(string)
|
|
|
|
if adminUser != "" {
|
|
|
|
return logical.ErrorResponse("Admin user not required for OTP type"), nil
|
|
|
|
}
|
2015-07-22 18:15:19 +00:00
|
|
|
|
2015-07-29 18:21:36 +00:00
|
|
|
defaultUser := d.Get("default_user").(string)
|
|
|
|
if defaultUser == "" {
|
|
|
|
return logical.ErrorResponse("Missing default user"), nil
|
|
|
|
}
|
2015-07-27 17:02:31 +00:00
|
|
|
|
2015-08-13 00:36:27 +00:00
|
|
|
entry, err = logical.StorageEntryJSON(fmt.Sprintf("roles/%s", roleName), sshRole{
|
2015-07-29 18:21:36 +00:00
|
|
|
DefaultUser: defaultUser,
|
|
|
|
CIDR: cidr,
|
|
|
|
KeyType: KeyTypeOTP,
|
2015-07-31 17:24:23 +00:00
|
|
|
Port: port,
|
2015-07-29 18:21:36 +00:00
|
|
|
})
|
|
|
|
} else if keyType == KeyTypeDynamic {
|
|
|
|
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
|
|
|
|
}
|
2015-07-27 17:02:31 +00:00
|
|
|
|
2015-08-06 18:48:19 +00:00
|
|
|
installScript := d.Get("install_script").(string)
|
|
|
|
if installScript == "" {
|
|
|
|
return logical.ErrorResponse("Missing install script"), nil
|
|
|
|
}
|
|
|
|
|
2015-07-29 18:21:36 +00:00
|
|
|
adminUser := d.Get("admin_user").(string)
|
|
|
|
if adminUser == "" {
|
|
|
|
return logical.ErrorResponse("Missing admin username"), nil
|
|
|
|
}
|
2015-07-27 17:02:31 +00:00
|
|
|
|
2015-07-29 18:21:36 +00:00
|
|
|
defaultUser := d.Get("default_user").(string)
|
|
|
|
if defaultUser == "" {
|
|
|
|
defaultUser = adminUser
|
2015-06-30 20:30:13 +00:00
|
|
|
}
|
2015-06-24 22:13:12 +00:00
|
|
|
|
2015-07-29 18:21:36 +00:00
|
|
|
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
|
|
|
|
}
|
2015-07-27 17:02:31 +00:00
|
|
|
|
2015-08-13 00:36:27 +00:00
|
|
|
entry, err = logical.StorageEntryJSON(fmt.Sprintf("roles/%s", roleName), sshRole{
|
2015-08-06 18:48:19 +00:00
|
|
|
KeyName: keyName,
|
|
|
|
AdminUser: adminUser,
|
|
|
|
DefaultUser: defaultUser,
|
|
|
|
CIDR: cidr,
|
|
|
|
Port: port,
|
|
|
|
KeyType: KeyTypeDynamic,
|
|
|
|
KeyBits: keyBits,
|
|
|
|
InstallScript: installScript,
|
2015-07-29 18:21:36 +00:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
return logical.ErrorResponse("Invalid key type"), nil
|
2015-07-06 20:56:45 +00:00
|
|
|
}
|
2015-06-30 20:30:13 +00:00
|
|
|
|
2015-06-24 22:13:12 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := req.Storage.Put(entry); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2015-06-19 00:48:41 +00:00
|
|
|
func (b *backend) pathRoleRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
2015-08-13 00:36:27 +00:00
|
|
|
roleName := d.Get("role").(string)
|
|
|
|
roleEntry, err := req.Storage.Get(fmt.Sprintf("roles/%s", roleName))
|
2015-06-19 00:48:41 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-07-02 21:23:09 +00:00
|
|
|
if roleEntry == nil {
|
2015-06-19 00:48:41 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
2015-07-27 17:02:31 +00:00
|
|
|
|
2015-07-02 21:23:09 +00:00
|
|
|
var role sshRole
|
|
|
|
if err := roleEntry.DecodeJSON(&role); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-07-27 17:02:31 +00:00
|
|
|
|
2015-07-31 17:24:23 +00:00
|
|
|
if role.KeyType == KeyTypeOTP {
|
|
|
|
return &logical.Response{
|
|
|
|
Data: map[string]interface{}{
|
|
|
|
"default_user": role.DefaultUser,
|
|
|
|
"cidr": role.CIDR,
|
|
|
|
"port": role.Port,
|
|
|
|
"key_type": role.KeyType,
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
} else {
|
|
|
|
return &logical.Response{
|
|
|
|
Data: map[string]interface{}{
|
|
|
|
"key": role.KeyName,
|
|
|
|
"admin_user": role.AdminUser,
|
|
|
|
"default_user": role.DefaultUser,
|
|
|
|
"cidr": role.CIDR,
|
|
|
|
"port": role.Port,
|
|
|
|
"key_type": role.KeyType,
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
2015-06-16 20:58:54 +00:00
|
|
|
}
|
|
|
|
|
2015-06-19 00:48:41 +00:00
|
|
|
func (b *backend) pathRoleDelete(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
2015-08-13 00:36:27 +00:00
|
|
|
roleName := d.Get("role").(string)
|
|
|
|
err := req.Storage.Delete(fmt.Sprintf("roles/%s", roleName))
|
2015-06-19 00:48:41 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-06-16 20:58:54 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2015-06-24 22:13:12 +00:00
|
|
|
type sshRole struct {
|
2015-08-06 18:48:19 +00:00
|
|
|
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"`
|
2015-06-24 22:13:12 +00:00
|
|
|
}
|
|
|
|
|
2015-06-16 20:58:54 +00:00
|
|
|
const pathRoleHelpSyn = `
|
2015-06-30 20:30:13 +00:00
|
|
|
Manage the 'roles' that can be created with this backend.
|
2015-06-16 20:58:54 +00:00
|
|
|
`
|
|
|
|
|
|
|
|
const pathRoleHelpDesc = `
|
2015-07-02 01:26:42 +00:00
|
|
|
This path allows you to manage the roles that are used to create
|
2015-07-27 17:02:31 +00:00
|
|
|
keys. These roles will be having privileged access to all
|
2015-07-02 01:26:42 +00:00
|
|
|
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' field takes comma seperated CIDR blocks. The 'admin_user'
|
|
|
|
should have root access in all the hosts represented by the 'cidr'
|
|
|
|
field. When the user requests key for an IP, the key will be installed
|
|
|
|
for the user mentioned by 'default_user' field. The 'key' field takes
|
|
|
|
a named key which can be configured by 'ssh/keys/' endpoint.
|
2015-07-31 17:24:23 +00:00
|
|
|
|
|
|
|
Role Options:
|
|
|
|
|
|
|
|
-key_type This can be either 'otp' or 'dynamic'. 'otp' key requires
|
|
|
|
agent to be installed in target machine. Required field for
|
|
|
|
both types.
|
|
|
|
|
|
|
|
-key Name of the key registered using 'keys/' endpoint. Required
|
|
|
|
field for 'dynamic' type. Not applicable for 'otp' type.
|
|
|
|
|
|
|
|
-admin_user Username at the target which is having root privileges. This
|
|
|
|
username will be used to install keys for other unprivileged
|
|
|
|
users. Required field for 'dynamic' type. Not applicable for
|
|
|
|
'otp' type.
|
|
|
|
|
|
|
|
-default_user When keys are created using '/creds' endpoint with only the
|
|
|
|
IP address, by default, this username is used to create the
|
|
|
|
credentials. Required for 'otp' type. Optional for 'dynamic' type.
|
|
|
|
|
|
|
|
-cidr CIDR block for which is role is applicable for. Required field
|
|
|
|
for both types.
|
|
|
|
|
|
|
|
-port Port number for SSH connections. Default is '22'. Optional for
|
|
|
|
both types.
|
|
|
|
|
|
|
|
-key_bits Length of RSa dynamic key in bits. Optional for 'dynamic' type.
|
|
|
|
Not applicable for 'otp' type.
|
2015-08-06 21:00:50 +00:00
|
|
|
|
|
|
|
-install_script Script used to install and uninstall public keys in the target
|
|
|
|
machine. Required for 'dynamic' type. Not applicable for 'otp'
|
|
|
|
type.
|
|
|
|
[For Linux, refer https://github.com/hashicorp/vault/tree/master/
|
|
|
|
builtin/logical/ssh/scripts/key-install-linux.sh]
|
2015-06-16 20:58:54 +00:00
|
|
|
`
|