Vault SSH: Introduced allowed_users option. Added helpers getKey and getOTP

This commit is contained in:
vishalnayak 2015-08-13 14:18:30 -07:00
parent 9b1ea2f20c
commit 3958136a78
8 changed files with 121 additions and 45 deletions

View File

@ -3,6 +3,7 @@ package ssh
import (
"fmt"
"net"
"strings"
"time"
"github.com/hashicorp/vault/helper/uuid"
@ -40,6 +41,18 @@ 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(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
roleName := d.Get("role").(string)
@ -47,8 +60,6 @@ func (b *backend) pathCredsCreateWrite(
return logical.ErrorResponse("Missing role"), nil
}
username := d.Get("username").(string)
ipRaw := d.Get("ip").(string)
if ipRaw == "" {
return logical.ErrorResponse("Missing ip"), nil
@ -62,6 +73,7 @@ func (b *backend) pathCredsCreateWrite(
return logical.ErrorResponse(fmt.Sprintf("Role '%s' not found", roleName)), nil
}
username := d.Get("username").(string)
// Set the default username
if username == "" {
if role.DefaultUser == "" {
@ -70,11 +82,25 @@ func (b *backend) pathCredsCreateWrite(
username = role.DefaultUser
}
if role.AllowedUsers != "" {
// Check if the username is present in allowed users list.
err := validateUsername(username, role.AllowedUsers)
// If username is not present in allowed users list, check if it
// is the default username in the role. If neither is true, then
// that username is not allowed to generate a credential.
if err != nil && username != role.DefaultUser {
return logical.ErrorResponse("Username is not present is allowed users list."), nil
}
}
// Validate the IP address
ipAddr := net.ParseIP(ipRaw)
if ipAddr == nil {
return logical.ErrorResponse(fmt.Sprintf("Invalid IP '%s'", ipRaw)), nil
}
// Check if the IP belongs to the registered list of CIDR blocks under the role
ip := ipAddr.String()
ipMatched, err := cidrContainsIP(ip, role.CIDRList)
if err != nil {
@ -182,26 +208,26 @@ func (b *backend) GenerateSaltedOTP() (string, string) {
return str, b.salt.SaltID(str)
}
// Generates a salted OTP and creates an entry for the same in storage backend.
// 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) {
otp, otpSalted := b.GenerateSaltedOTP()
entry, err := req.Storage.Get("otp/" + otpSalted)
entry, err := b.getOTP(req.Storage, otpSalted)
// Make sure that new OTP is not replacing an existing one
for err == nil && entry != nil {
otp, otpSalted = b.GenerateSaltedOTP()
entry, err = req.Storage.Get("otp/" + otpSalted)
entry, err = b.getOTP(req.Storage, otpSalted)
if err != nil {
return "", err
}
}
entry, err = logical.StorageEntryJSON("otp/"+otpSalted, sshOTP{
newEntry, err := logical.StorageEntryJSON("otp/"+otpSalted, sshOTP{
Username: username,
IP: ip,
})
if err != nil {
return "", err
}
if err := req.Storage.Put(entry); err != nil {
if err := req.Storage.Put(newEntry); err != nil {
return "", err
}
return otp, nil

View File

@ -36,10 +36,8 @@ func pathKeys(b *backend) *framework.Path {
}
}
func (b *backend) pathKeysRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
keyName := d.Get("key_name").(string)
keyPath := fmt.Sprintf("keys/%s", keyName)
entry, err := req.Storage.Get(keyPath)
func (b *backend) getKey(s logical.Storage, n string) (*sshHostKey, error) {
entry, err := s.Get("keys/" + n)
if err != nil {
return nil, err
}
@ -51,10 +49,21 @@ func (b *backend) pathKeysRead(req *logical.Request, d *framework.FieldData) (*l
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathKeysRead(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
key, err := b.getKey(req.Storage, d.Get("key_name").(string))
if err != nil {
return nil, err
}
if key == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"key": result.Key,
"key": key.Key,
},
}, nil
}

View File

@ -46,6 +46,15 @@ func (b *backend) pathLookupWrite(req *logical.Request, d *framework.FieldData)
matchingRoles = append(matchingRoles, role)
}
}
// This result may potentially reveal more information than it is supposed to.
// 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
// authenticated, it will fail. There are no problems there. In a way this can
// be viewed as a feature. The client can ask for permissions to be given for
// a specific role if things are not working!
// Going forward, the role names should be filtered and only the roles which
// the client is authorized to see, should be returned.
return &logical.Response{
Data: map[string]interface{}{
"roles": matchingRoles,

View File

@ -9,8 +9,22 @@ import (
"github.com/hashicorp/vault/logical/framework"
)
const KeyTypeOTP = "otp"
const KeyTypeDynamic = "dynamic"
const (
KeyTypeOTP = "otp"
KeyTypeDynamic = "dynamic"
)
type sshRole struct {
KeyType string `mapstructure:"key_type" json:"key_type"`
KeyName string `mapstructure:"key" json:"key"`
KeyBits int `mapstructure:"key_bits" json:"key_bits"`
AdminUser string `mapstructure:"admin_user" json:"admin_user"`
DefaultUser string `mapstructure:"default_user" json:"default_user"`
CIDRList string `mapstructure:"cidr_list" json:"cidr_list"`
Port int `mapstructure:"port" json:"port"`
InstallScript string `mapstructure:"install_script" json:"install_script"`
AllowedUsers string `mapstructure:"allowed_users" json:"allowed_users"`
}
func pathRoles(b *backend) *framework.Path {
return &framework.Path{
@ -81,6 +95,17 @@ func pathRoles(b *backend) *framework.Path {
The inbuilt default install script will be for Linux hosts. For sample
script, refer the project's documentation website.`,
},
"allowed_users": &framework.FieldSchema{
Type: framework.TypeString,
Description: `
[Optional for both types]
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
usernames are to be allowed, then this list enforces it. If this field is
set, then credentials can only be created for default_user and usernames
present in this list.
`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -100,6 +125,9 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*
return logical.ErrorResponse("Missing role name"), nil
}
// Allowed users is an optional field, applicable for both otp and dynamic types.
allowedUsers := d.Get("allowed_users").(string)
defaultUser := d.Get("default_user").(string)
if defaultUser == "" {
return logical.ErrorResponse("Missing default user"), nil
@ -136,10 +164,11 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*
}
roleEntry = sshRole{
DefaultUser: defaultUser,
CIDRList: cidrList,
KeyType: KeyTypeOTP,
Port: port,
DefaultUser: defaultUser,
CIDRList: cidrList,
KeyType: KeyTypeOTP,
Port: port,
AllowedUsers: allowedUsers,
}
} else if keyType == KeyTypeDynamic {
keyName := d.Get("key").(string)
@ -178,6 +207,7 @@ func (b *backend) pathRoleWrite(req *logical.Request, d *framework.FieldData) (*
KeyType: KeyTypeDynamic,
KeyBits: keyBits,
InstallScript: installScript,
AllowedUsers: allowedUsers,
}
} else {
return logical.ErrorResponse("Invalid key type"), nil
@ -252,17 +282,6 @@ func (b *backend) pathRoleDelete(req *logical.Request, d *framework.FieldData) (
return nil, nil
}
type sshRole struct {
KeyType string `mapstructure:"key_type" json:"key_type"`
KeyName string `mapstructure:"key" json:"key"`
KeyBits int `mapstructure:"key_bits" json:"key_bits"`
AdminUser string `mapstructure:"admin_user" json:"admin_user"`
DefaultUser string `mapstructure:"default_user" json:"default_user"`
CIDRList string `mapstructure:"cidr_list" json:"cidr_list"`
Port int `mapstructure:"port" json:"port"`
InstallScript string `mapstructure:"install_script" json:"install_script"`
}
const pathRoleHelpSyn = `
Manage the 'roles' that can be created with this backend.
`

View File

@ -23,6 +23,23 @@ func pathVerify(b *backend) *framework.Path {
}
}
func (b *backend) getOTP(s logical.Storage, n string) (*sshOTP, error) {
entry, err := s.Get("otp/" + n)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result sshOTP
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathVerifyWrite(req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
otp := d.Get("otp").(string)
@ -38,23 +55,23 @@ func (b *backend) pathVerifyWrite(req *logical.Request, d *framework.FieldData)
}
otpSalted := b.salt.SaltID(otp)
entry, err := req.Storage.Get("otp/" + otpSalted)
// Return nil if there is no entry found for the OTP
otpEntry, err := b.getOTP(req.Storage, otpSalted)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var otpEntry sshOTP
if err := entry.DecodeJSON(&otpEntry); err != nil {
if otpEntry == nil {
return nil, nil
}
// Delete the OTP if found. This is what makes the key an OTP.
err = req.Storage.Delete("otp/" + otpSalted)
if err != nil {
return nil, err
}
// Return username and IP only if there were no problems uptill this point.
return &logical.Response{
Data: map[string]interface{}{
"username": otpEntry.Username,

View File

@ -25,11 +25,11 @@ fi
grep -vFf $2 $3 > temp_$2
# Contents of temporary file will be the contents of authorized_keys file.
cat temp_$2 > $3
cat temp_$2 | sudo tee $3
if [ $1 == "install" ]; then
# New public key is appended to authorized_keys file
cat $2 >> $3
cat $2 | sudo tee --append $3
fi
# Auxiliary files are deleted

View File

@ -104,13 +104,12 @@ func (b *backend) secretDynamicKeyRevoke(req *logical.Request, d *framework.Fiel
port := int(portRaw.(float64))
// Fetch the host key using the key name
hostKeyEntry, err := req.Storage.Get(fmt.Sprintf("keys/%s", hostKeyName))
hostKey, err := b.getKey(req.Storage, hostKeyName)
if err != nil {
return nil, fmt.Errorf("key '%s' not found error:%s", hostKeyName, err)
}
var hostKey sshHostKey
if err := hostKeyEntry.DecodeJSON(&hostKey); err != nil {
return nil, fmt.Errorf("error reading the host key: %s", err)
if hostKey == nil {
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

View File

@ -35,10 +35,7 @@ func (b *backend) secretOTPRevoke(req *logical.Request, d *framework.FieldData)
return nil, fmt.Errorf("secret is missing internal data")
}
otpSalted := b.salt.SaltID(otp)
otpPath := fmt.Sprintf("otp/%s", otpSalted)
err := req.Storage.Delete(otpPath)
err := req.Storage.Delete("otp/" + b.salt.SaltID(otp))
if err != nil {
return nil, err
}