mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-04 20:06:27 +02:00
Vault SSH: Introduced allowed_users option. Added helpers getKey and getOTP
This commit is contained in:
parent
9b1ea2f20c
commit
3958136a78
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
`
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user