usermgr

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 23, 2016 License: MIT Imports: 28 Imported by: 0

README

usermgr

Managing administrative access to production systems is a pain. We all know that having accurate administrative access is important for security. Create a accounts too slowly and you incentivize account sharing. Disable accounts too slowly and you risk violating least-privilege (or worse). At the same time, centralized, online systems can represent a single point of failure for your application.

Usermgr is a tool to turn access to production systems from a pain in the butt into ponies and rainbows.

Usermgr access rules are determined by a single signed file that can be cached locally so it doesn't create a dependency on any central system.

Problems it fixes:

  • Creating local unix accounts
  • Managing user's SSH authorized keys
  • Two factor authentication with Yubikey or TOTP
  • Shell logging
  • Managing the sudoers file

A web interface and command line tools are available for managing users, enrollment and user self-service. Each node in the environment has access to the account database which is cryptographically signed and centrally managed.

Getting started

0. Install

Download an appropriate binary for your system:

# curl -o /opt/usermgr/bin/usermgr https://github.com/crewjam/usermgr/releases/download/XXX/usermgr.$(uname -s).$(uname -m) 
# chmod +x /opt/usermgr/bin/usermgr

From source:

go install github.com/crewjam/usermgr
1. Generate a key pair

The account database can be stored in Amazon S3 or on the local file system. (use the file:// url-scheme). usermgr uses the username and password fields of the URL to store the public and private keys for the database, respectively.

$  usermgr keygen
admin key: Ulc7w67dHOagHVBWf18fmTAAOCs3dG0mql0NTTjDP2xQHNgZQjAo6Oy2aJie89TdOR10vg-cx-d0POwpm8tB5K-FMguXPr8b_zS3_fvTW1k16IMbs_aCoQ8u82eLcyB8A_CwAvsoRCVGmMzzBRtMJtquskeEMidS6AGMDvcteDc
host key: Ulc7w67dHOagHVBWf18fmTAAOCs3dG0mql0NTTjDP2xQHNgZQjAo6Oy2aJie89TdOR10vg-cx-d0POwpm8tB5A

This produces two keys. Posession of the admin key allows you to edit the file and resign it. Posession of the host key allows you to read the file, but not edit it. All your servers will have the host key, but generally only the one running the web interface will have the admin key.

2. Run the web interface (optional)

The web interface requires web users to be authenticated with an external mechanism. You can use oauth or header.

For header authentication, another server (i.e. Apache or nginx) handles the authentication and places the user name in the X-Remote-User header. For OAuth authentication, you must provide parameters for the OAuth provider.

 $ UM_ADMIN_KEY=Ulc7w67dHOagHVBWf18fmTAAOCs3dG0mql0NTTjDP2xQHNgZQjAo6Oy2aJie89TdOR10vg-cx-d0POwpm8tB5K-FMguXPr8b_zS3_fvTW1k16IMbs_aCoQ8u82eLcyB8A_CwAvsoRCVGmMzzBRtMJtquskeEMidS6AGMDvcteDc \
 UM_STORE=file:///var/usermgr/ \
 UM_AUTH=oauth://google?client_id=XXX.apps.googleusercontent.com&client_secret=xYxYxY&email_suffix=@example.com \
 UM_TOKEN_KEY=someRandomThing \
 UM_URL=https://users.example.com \
 usermgr web

The web interface will automatically create users the first time they navigate to the web interface. Those newly created users will not be part of any groups, so they won't have access to any systems. (The first user is automatically added to the usermgr-admin group. This is the only special group. Users that are members of this group are allowed to create or destroy users, edit group membership, and modify other users besides themselves.)

The only storage scheme supported is file. (Your contributions in this area are welcome!)

The only auth scheme supported in oauth. (Your contributions in this area are welcome!). The following query parameters are supported for oauth:

  • client_id - The OAuth2 client id.
  • client_secret - The OAuth2 client secret.
  • scope - which OAuth2 scopes to request. Specify multiple times for multiple scopes. The default is openid, profile and email.
  • auth_url - The URL where OAuth2 auth requests are sent
  • token_url - The URL where OAuth2 tokens are produced
  • user_info_url - An OpenID-compatible user info request URL.
  • email_suffix - If present require that the email address end with the specified suffix.

If you specify google as the hostname in the auth URL, then the default auth_url, token_url and user_info_url are filled in for you.

Setting up Managed Systems

On each system that you want to manage you'll need to install usermgr and configure the system.

1. Configure usermgr

On managed systems you do not use the editing URL, instead use the read-only URL which contains only the public key. Create a file called /etc/usermgr.conf that looks something like this:

  URL = "https://users.example.com"
  HostKey = "m_NiqMyWkkgOi1sT4uMCnp5kYuNanescRkRr3DP29FUAAgQGCAoMDhASFBYYGhweICIkJigqLC4wMjQ2ODo8Pg"
  LoginMFARequried = false
2. Configure the sshd

Tell sshd to ignore the user's ~/.ssh/authorized_keys file and instead invoke a command to determine which keys to use.

echo "AuthorizedKeysCommand /opt/bin/usermgr.sshkeys" >> /etc/ssh/sshd_config
echo "AuthorizedKeysCommandUser nobody" >> /etc/ssh/sshd_config
ln -s /opt/bin/usermgr /opt/bin/usermgr.sshkeys

Note: when you invoke usermgr as usermgr.sshkeys it is equivalent to invoking usermgr sshkeys. This mini-hack is needed because AuthorizedKeysCommand expects a single binary, not a command line.

3. Configure usermgr to be a login shell.

If you want to use multi-factor authentication or logging, you can replace each user's login shell with usermgr

(
  echo 'Defaults log_output'
  echo 'Defaults!/usr/bin/sudoreplay !log_output'
  echo 'Defaults!/sbin/reboot !log_output'
  echo 'Defaults env_keep += "SSH_CLIENT SSH_CONNECTION"'
) > /etc/sudoers.d/replay
ln -s /opt/bin/usermgr /opt/bin/usermgr.shell
chsh -s /opt/bin/usermgr.shell bob

usermgr shell enforces the second authentication factor. It also enforces shell-logging by invoking bash via sudo to the current user. (Note: invoking sudo in this context doesn't change the user's privilege level, it only allows terminal logging to happen.)

4. Arrange for usermgr sync to run every couple of minutes.

This keeps the local copy of the account database up to date, creates or removes local users, and keeps the sudoers file in sync.

echo '*/5 * * * /opt/usermgr/bin/usermgr.sync' > /etc/cron.d/usermgr

Multi-factor Authentication

Usermgr supports yubikey, and Google Authenticator (TOTP) for multi-factor authentication. You can (should!) also create backup codes that allow you to connect in the event of a failure of the authentication service or your multi-factor device.

Yubikey

We support yubikeys in the (default) Yubikey-OTP mode.

Register your yubikey by trying it out at https://demo.yubico.com/. If it isn't use the "Upload to Yubico" button in the Yubico Personalization Tool (or some other means, as documented) so that yubicloud knows about your key.

In the web interface, click "Enroll Yubikey" and press the button on your key.

Yubikeys are validated online by each server at login time. The Yubico client ID and secret must be entered in the web interface and are then available to each host.

Google Authenticator / TOTP

Install the Google Authenticator app for Android or iOS.

In the web interface, click "Enroll Smartphone" and scan the QR code with the Google Authenticator app.

TOTP requires a secret be shared between the system doing the authenticating and the device (mobile phone, etc.). We'd prefer not to share the TOTP secret with every host you log in to, only the auth server. At the same time we must not require that the central server be online to authenticate. So instead, on the auth server, usermgr pregenerates a bunch of TOTP auth codes for every user and store the resulting hashes in users.pem which is distributed to all the hosts. Thus the hosts can authenticate TOTP codes by comparing hashes even if the auth server is offline for a short time. If the auth server is offline long enough that the host runs out of codes, then TOTP authentication will fail. By default, every hour, two hours worth of codes are generated.

Backup Codes

Click "Generate Backup Code". The web interface will display a code and give you a chance to copy it down. Put it in a safe place, this is the only chance you'll have to use the backup code.

Backup codes are hashed before they are stored in users.pem.

Configuration Reference

Here is a commented example configuration file:

# The URL where the account database is stored. This URL can also
# point to a storage service, i.e. "https://s3.amazonaws.com/example/users.pem"
URL = "https://users.example.com/users.pem"

# Specifies the host key used to decrypt and verify the database
HostKey = ""

# Specifies the path where a local copy of the account database is stored.
# (Default: /var/lib/usermgr)
CacheDir = "/var/lib/usermgr"

# Specifies which groups a user must be part of in order to enable their 
# account. Comma separated list. (Default: users)
LoginGroups = "myapp-admin,myapp-user"

# Specifies which groups a user must be part of in order to enable them
# to sudo to root. Comma separated list. (Default: wheel)
SudoGroups = "myapp-admin"

# If true then all remote users must specify an MFA token to login.
# (Default: false)
LoginMFARequried = true

FAQ

How should I secure users.pem?

The file is encrypted such that only holders of the host key or admin key can read it. You should probably treat this file a bit like you would treat /etc/shadow -- private, but not secret. Even if an attacker had posession of the file and the host key, it does not contain any directly usable credentials, only one-way hashes of credentials.

How should I secure my host key.

It is private but not secret, like /etc/shadow. The host key unlocks access to users.pem which would give an attacker access to a list of your users, their privileges and public keys. But it does not contain directly usable credentials and thus if it fell into the wrong hands would not allow an attacker additional access.

How should I secure my admin key.

This is the key to your kingdom, so you should protect it well. The admin key allows editing the user database, which could be used to add new user accounts or grant additional privileges to user accounts. The admin key also allows access to TOTP secrets.

What cryptographic algorithms are used?

  • To sign the user database, ED25519
  • To encrypt the secrets in the user database, we use NaCL which uses XSalsa20 and Poly1305.
  • To hash the backup keys, Scrypt.

What happens if the web server does down?

You cannot use it to modify the accounts database any more. Hosts that have users.pem cached locally will continue to use it.

Eventually, the pregenerated TOTP codes will expire and TOTP authentication will stop working. Yubikey authentication requires access to yubikey's servers so it will continue to work. Backup codes continue to work.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrIncorrectCode = errors.New("code is invalid")
View Source
var ErrIncorrectKeyFormat = errors.New("incorrect key format")

Functions

func ExpectedCode

func ExpectedCode(counter uint64, secretBytes []byte) string

func SyncSudoers

func SyncSudoers(ud *UsersData, loginGroups []string, sudoGroups []string, dryRun bool) error

func SyncUsers

func SyncUsers(ud *UsersData, groups []string, dryRun bool, stdout io.Writer) error

func ValidateCode

func ValidateCode(user User, code string, yubicoClientID, yubicoSecretKey string) error

ValidateCode checks that the specified code is a valid MFA code for the specified user. The supplied code can be one of the following:

  • A TOTP code which is checked against validateURL
  • A Yubikey code which is checked against validateURL
  • A backup code which is compared to the hashed list of backup codes for the user

Return nil if the code is valid, or an error otherwise.

Types

type AdminKey

type AdminKey struct {
	HostKey
	AdminPrivateKey [32]byte
	HostPublicKey   [32]byte
}

func GenerateKeyPair

func GenerateKeyPair() AdminKey

GenerateKeyPair returnes a new key pair

func (AdminKey) MarshalJSON

func (ak AdminKey) MarshalJSON() ([]byte, error)

func (AdminKey) MarshalText

func (ak AdminKey) MarshalText() (text []byte, err error)

func (AdminKey) String

func (ak AdminKey) String() string

func (*AdminKey) UnmarshalJSON

func (ak *AdminKey) UnmarshalJSON(b []byte) error

func (*AdminKey) UnmarshalText

func (ak *AdminKey) UnmarshalText(text []byte) error

type BackupCode

type BackupCode struct {
	CreateTime time.Time `json:"create_time,omitempty"`
	Salt       []byte    `json:"salt,omitempty"`
	Hash       []byte    `json:"hash,omitempty"`
}

func NewBackupCode

func NewBackupCode(code string) BackupCode

func (BackupCode) Matches

func (bc BackupCode) Matches(userCode string) bool

type HostKey

type HostKey struct {
	AdminPublicKey [32]byte
	HostPrivateKey [32]byte
}

func (HostKey) MarshalJSON

func (hk HostKey) MarshalJSON() ([]byte, error)

func (HostKey) MarshalText

func (hk HostKey) MarshalText() (text []byte, err error)

func (HostKey) String

func (hk HostKey) String() string

func (*HostKey) UnmarshalJSON

func (hk *HostKey) UnmarshalJSON(b []byte) error

func (*HostKey) UnmarshalText

func (hk *HostKey) UnmarshalText(text []byte) error

type TOTPCode

type TOTPCode struct {
	Time time.Time `json:"time"`
	Salt []byte    `json:"salt"`
	Hash []byte    `json:"hash"`
}

type TOTPDevice

type TOTPDevice struct {
	Name       string    `json:"name,omitempty"`
	CreateTime time.Time `json:"create_time,omitempty"`

	SecretNonce     []byte `json:"secret_nonce"`
	SecretEncrypted []byte `json:"secret_encrypted"`

	Codes []TOTPCode `json:"codes"`
}

TOTPDevice represents an enrolled TOTP device. Each device has a secret that is shared between us and the device which is used to generate time-based codes. The secret is stored here encrypted to the AdminKey, so it is only accessible to systems holding the admin key.

To allow systems not holding the AdminKey to validate TOTP secrets a series of upcoming codes are stored in `Codes`. Since these codes are essentially passwords we can store them that way, using scrypt to generate a one-way hash which can be compared against what the user enters.

func (*TOTPDevice) GenerateCodes

func (d *TOTPDevice) GenerateCodes(startTime, endTime time.Time, adminKey AdminKey) error

func (TOTPDevice) Secret

func (d TOTPDevice) Secret(adminKey AdminKey) (string, error)

func (*TOTPDevice) SetSecret

func (d *TOTPDevice) SetSecret(adminKey AdminKey, secret string)

func (TOTPDevice) VerifyCode

func (d TOTPDevice) VerifyCode(now time.Time, skew time.Duration, userCode string) error

type User

type User struct {
	Name           string          `json:"name"`
	RealName       string          `json:"real_name,omitempty"`
	Email          string          `json:"email,omitempty"`
	Groups         []string        `json:"groups,omitempty"`
	AuthorizedKeys []string        `json:"authorized_keys,omitempty"`
	Yubikeys       []YubikeyDevice `json:"yubikeys,omitempty"`
	BackupCodes    []BackupCode    `json:"backup_codes,omitempty"`
	TOTPDevices    []TOTPDevice    `json:"totp_devices,omitempty"`
}

User represents a single user

func (User) InAnyGroup

func (u User) InAnyGroup(groupNames []string) bool

InAnyGroup returns true if the user is a member of the specified group

func (User) InGroup

func (u User) InGroup(groupName string) bool

InGroup returns true if the user is a member of the specified group

type UsersData

type UsersData struct {
	Users               []User `json:"users"`
	YubikeyClientID     string `json:"yubikey_client_id,omitempty"`
	YubikeyClientSecret string `json:"yubikey_client_secret,omitempty"`
}

UsersData is the document that contains all the authoritative data about users, their keys, group membership, etc.

func GetLocalCache

func GetLocalCache(path string, hostKey HostKey) (*UsersData, error)

GetLocalCache returns the local cached data in path if it is valid. It does not attempt to update the cache.

func LoadUsersData

func LoadUsersData(data []byte, hostKey HostKey) (*UsersData, error)

LoadUsersData reads signedData, verifies that it was signed by the specified publicKey and if all is well, returns a new instance of UserData.

func UpdateLocalCache

func UpdateLocalCache(path string, upstreamURL string, hostKey HostKey) (*UsersData, error)

UpdateLocalCache fetches the user data from upstreamURL if it is unchanged. If the response is valid, the cache files in path are replaced and the new data are returned.

func (*UsersData) Delete

func (ud *UsersData) Delete(userName string)

Delete removes a user from the list of users

func (UsersData) GetUserByName

func (ud UsersData) GetUserByName(name string) *User

GetUserByName returns the user having the specified name or nil if no such user exists.

func (*UsersData) Set

func (ud *UsersData) Set(user User)

Set adds or replaces `user` to the list of users.

func (UsersData) SignedString

func (ud UsersData) SignedString(adminKey AdminKey) ([]byte, error)

SignedString returns a serialized version of the user database signed with the given key.

type YubikeyDevice

type YubikeyDevice struct {
	Name       string    `json:"name,omitempty"`
	CreateTime time.Time `json:"create_time,omitempty"`
	DeviceID   string    `json:"device_id,omitempty"`
}

Directories

Path Synopsis
cmd
web
package web implemented the web interface for usermgr
package web implemented the web interface for usermgr

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL