beesly is a microservice for authenticating users with PAM.
It provides an alternative method for authenticating and authorizing users' access to internal applications and services. Support for custom PAM services facilitates the reuse of existing integrations (SSSD) with directory servers (Active Directory, IdM, FreeIPA, OpenLDAP, etc.) and third party services (Duo Security). Group membership is returned for authenticated users allowing the addition of Role-Based Access Control for custom applications and services without the need to learn the intricacies of LDAP or Kerberos.
beesly was developed in Python 3.6 using the Flask microframework.
- Authenticate users with custom PAM services
- Role-Based Access Control (RBAC)
- Integration with Duo Security for 2-Factor Authentication (2FA)
- short-lived JSON Web Tokens (JWT)
- Rate limits to prevent abuse
Pipenv is used to manage dependencies.
beesly requires superuser privileges to authenticate users when running in its default configuration because it uses the PAM login service which uses the pam_unix.so
PAM module. This module adds a 2 second delay for failed authentications which can be disabled by using the nodelay option. It's encouraged to use a custom PAM service that does not use this module.
If using a custom PAM service, the configuration file should be placed in /etc/pam.d
with 0644
permissions. See examples/beesly.pam
for an example of a custom PAM service that uses SSSD and does not require superuser privileges.
Flask-Limiter is used to implement rate limits. It can use in-memory, Redis, or Memcached as a storage backend.
Run beesly using gunicorn:
$ gunicorn -c gconfig.py --preload -b '127.0.0.1:8000' -w 4 serve:app
For production deployment, run gunicorn behind nginx and use TLS.
Authenticating a user:
$ curl -X POST http://127.0.0.1:8000/auth -d '{"username":"dwight.schrute@dundermifflin.com", "password":"BearsBeetsBattlestarGalactica"}'
{
"auth": true,
"groups": [
"Sales",
"Assistant_Regional_Manager",
"Assistant_to_the_Regional_Manager"
],
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiZWVz...",
"message": "Authentication successful"
}
A successful authentication will return the groups that the user is a member of facilitating RBAC.
If Duo Security 2FA is configured, the user will have to acknowledge a push notification for the authentication to succeed.
The payload of the generated JWT contains the following claims:
{
'exp': 1489344336.296496,
'groups': [
"Sales",
"Assistant_Regional_Manager",
"Assistant_to_the_Regional_Manager"
],
'iat': 1489343436.296496,
'iss': 'beesly',
'sub': 'dwight.schrute@dundermifflin.com',
'x': '2soDlgCPC0RFuxR0'
}
Note: a JWT is returned only when JWT_MASTER_KEY
is configured.
Renewing an existing JWT that has not expired:
$ curl -X POST http://127.0.0.1:8000/renew -d '{"username":"dwight.schrute@dundermifflin.com","jwt":"2NzUuMjEyMzAyLCJncm91cHMiOm51bGwsInN1Yi..."}'
{
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiZWvR...",
"message": "JWT successfully renewed"
}
Verifying the validity of a JWT:
$ curl -X POST http://127.0.0.1:8000/verify -d '{"jwt":"2NzUuMjEyMzAyLCJncm91cHMiOm51bGwsInN1Yi..."}'
{
"message": "JWT successfully verified",
"valid": true
}
Note: /renew
and /verify
endpoints are only available if JWT_MASTER_KEY
is configured.
Retrieving information about the running application:
$ curl http://127.0.0.1:8000/service
{
"app": {
"name": "beesly",
"uptime": 9,
"version": "0.1.0"
},
"aws": {
"availability_zone": "us-west-2b",
"image_id": "ami-d2c924b2",
"instance_id": "i-0ef224f23818bd413",
"instance_type": "t2.micro",
"region": "us-west-2"
},
"system": {
"hostname": "ip-172-31-36-157",
"memory": "991 MB",
"processors": 1,
"uptime": 10647
}
}
Note: EC2 metadata is returned only when running on AWS EC2.
Monitoring the health of the application:
$ curl http://127.0.0.1:8000/service/health
{
"beesly": "OK"
}
Swagger UI is integrated into beesly and available at /service/docs/index.html
when running in DEV
mode.
The following environment variables are used to modify the running configuration of this app:
Variable | Type | Required | Default Value | Explanation |
---|---|---|---|---|
DEV | Boolean | No | False | Set to True to enable debug logging and Swagger UI. |
PAM_SERVICE | String | No | login | The name of the PAM service to authenticate users with. |
JWT_MASTER_KEY | String | No | The master key to use when generating JSON Web Tokens. Must be between 10 - 64 characters in length. |
|
JWT_ALGORITHM | String | No | HS256 | The HMAC algorithm to use when generating JWTs. One of: * HS256 * HS384 * HS512 |
JWT_VALIDITY_PERIOD | Interger | No | 900 | The validity period in seconds for generated JWTs. |
STATSD_HOST | String | No | localhost | The hostname or IP address of the statsd collector. |
STATSD_PORT | Integer | No | 8125 | The UDP port of the statsd collector. |
RATELIMIT_ENABLED | Boolean | No | True | Set to False to disable rate limiting. |
RATELIMIT_STRATEGY | String | No | fixed-window | The rate limiting strategy to use. One of: * fixed-window * fixed-window-elastic-expiry * moving-window |
RATELIMIT_STORAGE_URL | String | No | memory:// | The URL for the storage backend used for rate limiting. Refer to limits documentation for correct syntax. |
Note: The moving-window
rate limiting strategy can only be used with in-memory
or Redis
storage.
beesly can integrate with Duo Security to provide 2-factor authentication
using the pam_duo.so
PAM module.
Create a custom PAM service:
$ sudo vim /etc/pam.d/beesly
auth required pam_env.so
auth requisite pam_duo.so
auth sufficient pam_unix.so nodelay
auth required pam_deny.so
Configure duo_unix
to send push notifications to the user's phone:
$ sudo vim /etc/duo/pam_duo.conf
; Duo Unix config
; https://duo.com/docs/duounix
[duo]
ikey=
skey=
host=
failmode=secure
autopush=yes
prompts=1
https_timeout=2
Users will have to acknowledge the push notification for authentication to succeed.
beesly can optionally return short-lived JSON Web Tokens upon successful user authentication. To add JWT support, a secret master key must be set via the JWT_MASTER_KEY
environment variable. It must be between 10 - 64 characters in length.
blake2b key derivation function from pynacl is used to create a unique signing key for each generated token:
master_key = app.config["JWT_MASTER_KEY"]
unique_salt = nacl.encoding.URLSafeBase64Encoder.encode(nacl.utils.random(12))
signing_key = blake2b(b'', key=master_key, salt=unique_salt, person=username)
By default, each JWT is valid for 15 minutes. JWTs can be renewed by sending a POST request to /renew
with the payload containing the username and their valid token. JWTs can be verified by sending a POST request to /verify
with the payload containing the token.
The Python statsd client is used to export application metrics with the prefix beesly
.
The following metrics are exported:
Name | Type | Explanation |
---|---|---|
pam_auth | Meter | Time taken by PAM to authenticate a user |
auth_success | Counter | User authentication succeeded |
auth_failed | Counter | User authentication failed |
jwt_generated | Counter | a JWT was successfully generated |
jwt_renewed | Counter | a JWT was successfully renewed |
jwt_verified | Counter | a JWT was successfully verified |
See examples/telegraf.conf
for how to configure telegraf as a statsd collector sending metrics to influxdb.
nose2 is used for testing. Tests are located in beesly/tests
.
The tests require valid credentials to a local user to execute correctly. Credentials for this user can be set using environment variables:
$ export TEST_USERNAME=example
$ export TEST_PASSWORD=helloworld
To run the test suite:
$ sudo make test