Coder Social home page Coder Social logo

cerberus-python-client's Introduction

Cerberus Python Client

Python package codecov PyPI version

This is a Python based client library for communicating with Cerberus via HTTPS and enables authentication schemes specific to AWS and Cerberus.

This client currently supports read-only operations (write operations are not yet implemented, feel free to open a pull request to implement write operations)

To learn more about Cerberus, please visit the Cerberus website.

Installation

** Note: This is a Python 3 project but should be compatible with python 2.7.

Clone this project and run one of the following from within the project directory:

python3 setup.py install

or for python 2.7

python setup.py install

Or simply use pip or pip3

pip3 install cerberus-python-client

Alternatively, add cerberus-python-client in the install_requires section of your project's setup.py. Then run one of the following from within your projects directory:

python3 setup.py install

or for python 2.7

python setup.py install

Usage

Import the Client:

from cerberus.client import CerberusClient

Instantiate the Client

IAM Role Authentication(Local, EC2, ECS, Lambda, etc.):

client = CerberusClient('https://my.cerberus.url')

Note: If authenticating from the China AWS partition you must specify the AWS region you are using in China.

client = CerberusClient('https://my.cerberus.url', region="cn-northwest-1")

If no region is specified us-west-2 is assumed and STS authentication using China IAM roles will fail.

User Authentication:

client = CerberusClient('https://my.cerberus.url', username, password)

Authentication Through an Assumed Role:

sts = boto3.client('sts')
role_data = sts.assume_role(RoleArn = 'arn:aws:iam::0123456789:role/CerberusRole', RoleSessionName = "CerberusAssumeRole")
creds = role_data['Credentials']

# Cerberus can be passed a botocore or boto3 session to use for authenticating with the Cerberus Server.
cerberus_session = boto3.session.Session(
    region_name = 'us-east-1',
    aws_access_key_id = creds['AccessKeyId'],
    aws_secret_access_key = creds['SecretAccessKey'],
    aws_session_token = creds['SessionToken']
)

client = CerberusClient(cerberus_url='https://my.cerberus.url', aws_session=cerberus_session)

Activate Log Messages

from cerberus.client import CerberusClient
import logging

# Logging has to be imported and the root level logger needs to be configured
#  before you instantiate the client
logging.basicConfig(level=logging.INFO)

client = CerberusClient('https://my.cerberus.url')

Surpress debug messages

# By default the Cerberus client will log some helpful messages to stderr
# setting verbose to False will surpress these messages.
client = CerberusClient('https://my.cerberus.url', verbose=False)

Read Secrets from Cerberus

To list what secrets are in a safe deposit box:

client.list_secrets('app/safe-deposit-box')

To get a secret for a specific key in a safe deposit box:

client.get_secrets_data("app/path/to/secret")["secretName"]

** Note: If you need to get more than one key, it's best to use the following example to get all the secrets at once instead of calling get_secrets_data multiple times.

To get all the secrets for an safe deposit box:

client.get_secrets_data("app/path/to/secret")

To view the available versions of a secret in a safe deposit box:

client.get_secret_versions("app/path/to/secret")

#optionally you can pass a limit and offset to limit the output returned and paginate through it.

client.get_secret_versions("app/path/to/secret", limit=100, offset=0)

To get a secret at a specific version:

client.get_secrets_data("app/path/to/secret", version='<version id>')

Write Secrets to Cerberus

To write a secret to a safe deposit box:

client.put_secret("app/path/to/secret", {'key-name': 'value to store'})

By default put_secret will attempt to merge the dictionary provided with what already exists in the safe deposit box. If you want to overwrite the stored dictionary in the safe deposit box called put_secret with merge=False.

client.put_secret("app/path/to/secret", {'new-keys': 'new values'}, merge=False)

View roles and categories

Roles are the permission scheme you apply to an AD group or IAM roles to allow reading or writing secrets. To view the available roles and their ids:

client.get_roles()

This will return a list of dictionaries with all the roles.

A convience function is available that will return a dictionary with the role names as keys, and the role id as values.

client.list_roles()

If you know the role name you need are are trying to get the id for it:

client.get_role('role-name')

That will return a string containing the role id.

Categories are for organizing safe deposit boxes. To list the available categories:

client.get_categories()

Read files from Cerberus

To list files in Cerberus

client.list_files('category/path/')

To download a file and its metadata

client.get_file('category/sdb/path/to/file.example')

## Returns
{'Date': 'Thu, 10 May 2018 00:37:47 GMT',
 'Content-Type': 'application/octet-stream; charset=UTF-8',
 'Content-Length': '36',
 'Connection': 'keep-alive',
 'Content-Disposition': 'attachment; filename="file.example"',
 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
 'X-B3-TraceId': 'f403214321',
 'Content-Encoding': 'gzip',
 'filename': 'file.example',
 'data': b'example file. With binary data \xab\xba\xca\xb0'}

The key 'filename' is generated by the library. The key 'data' contains the binary file data

Download a file at a specific version

client.get_file('category/sdb/path/to/file.example', 'version id')

To view available versions for a file

client.get_file_versions('category/sdb/path/to/file.example')

To download just the file data

client.get_file_data('category/sdb/path/to/file.example')

## Returns
b'example file. With binary data \xAB\xBA\xCA\xB0'

To download just the file metadata

client.get_file_metadata('category/sdb/path/to/file.example')

## Returns
{'Date': 'Thu, 10 May 2018 00:57:00 GMT',
 'Content-Type': 'application/octet-stream; charset=UTF-8',
 'Content-Length': '36',
 'Connection': 'keep-alive',
 'Content-Disposition': 'attachment; filename="test.py"',
 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
 'X-B3-TraceId': 'beede324324324'}

Upload a file to Cerberus

Uploading a file to Cerberus

## put_file('SDB Path', 'file name', file handle to file you want to upload)
client.put_file('category/sdb/path/to/file.example', open('file.example', 'rb'))

For the file you open, please make sure it's opened in binary mode, otherwise the size calculations for how big it is can be off.

Delete a file in Cerberus

client.delete_file('category/sdb/path/to/file.example')

Create a Safe Deposit Box

To create a new Safe Deposit Box:

client.create_sdb(
  'Name of Safe Deposit Box',
  'category_id',
  'owner_ad_group',
  description = 'description',
  user_group_permissions=[{ 'name': 'ad-group', 'role_id': 'role id for permissions'}],
  iam_principal_permissions=[{'iam_principal_arn': 'arn:aws:iam:xxxxxxxxxx:role/role-name', 'role_id': 'role id for permissions'}]
)

You will recieve a json response giving you the details of your new safe deposit box. As a note, you usually have to refresh your tokens before you are able to write secrets to the new safe deposit box.

Update a Safe Deposit Box

To update a Safe Deposit Box:

client.update_sdb(
  'sdb_id',
  owner='owner ad group',
  description='description of safe deposit box',
  user_group_permissions=[{'name': 'ad group', 'role_id': 'role id for permissions'}],
  iam_principal_permissions=[{'iam_principal_arn': 'arn:aws:iam:xxxxxxxxxx:role/role-name', 'role_id': 'role id for permissions'}]
)

When updating, if you don't specify a parameter, the current values in the safe deposit box will be kept. So you don't need to include the description, or iam_principal_permissions if you're only updating the user_group_permissions. Unlike put_secret, no attempt is made to merge the permissions dictionaries for you, so if you are adding a new user group, you must include the already existing user groups you want to keep in your update call.

Get a Cerberus Authentication token

If you do not want to read a secret, but simply want an authentication token, then you can use one of the <type>_auth.py classes to retrieve a token.

You can also use the CerberusClient class.

  • IAM Role Authentication
from cerberus.aws_auth import AWSAuth
token = AWSAuth('https://my.cerberus.url').get_token()
  • User Authentication
from cerberus.user_auth import UserAuth
token = UserAuth('https://my.cerberus.url', 'username', 'password').get_token()'

Lambdas

Generally it does NOT make sense to store Lambda secrets in Cerberus for two reasons:

  1. Cerberus cannot support the scale that lambdas may need, e.g. thousands of requests per second
  2. Lambdas will not want the extra latency needed to authenticate and read from Cerberus

A better solution for Lambda secrets is using the encrypted environmental variables feature provided by AWS.

Another option is to store Lambda secrets in Cerberus but only read them at Lambda deploy time, then storing them as encrypted environmental variables, to avoid the extra Cerberus runtime latency.

Lambda examples

Get secrets from Cerberus using IAM Role (execution role) ARN. It's a good idea to cache the secrets since AWS reuses Lambda instances.

from cerberus.client import CerberusClient
secrets = None
def lambda_handler(event, context):
    if secrets is None:
        client = CerberusClient('https://my.cerberus.url')
        secrets = client.get_secrets_data("app/yourapplication/dbproperties")['dbpasswd']

Admin

A Cerberus admin (not to be confused with SDB owners) may perform additional tasks such as getting SDB metadata.

Metadata

Get all SDB metadata from Cerberus.

from cerberus.client import CerberusClient
metadata = CerberusClient('https://my.cerberus.url').get_metadata()

Get SDB metadata of a specific SDB from Cerberus.

from cerberus.client import CerberusClient
metadata = CerberusClient('https://my.cerberus.url').get_metadata(sdb_name='my sdb')

Running Tests

You can run all the unit tests using nosetests. Most of the tests are mocked.

$ nosetests --verbosity=2 tests/

Local Development

The easiest way to locally test the Python client is to first authenticate with Cerberus through the dashboard, then use your dashboard authentication token to make subsequent calls. See examples below.

from cerberus.client import CerberusClient
client = CerberusClient('https://my.cerberus.url') # This will work on an EC2 instance. But it will fail on local when it tries to call the metadata endpoint.

Without changing any code, set the CERBERUS_TOKEN system environment variable:

$ export CERBERUS_TOKEN='mytoken'
from cerberus.client import CerberusClient
client = CerberusClient('https://my.cerberus.url') # On local, the client will pick up the environment variable that was set earlier. When it's deployed to an EC2 instance that doesn't have the `CERBERUS_TOKEN` system environment variable, it'll automatically switch to authenticating using the metadata endpoint.

Alternatively, you can pass in the token directly.

from cerberus.client import CerberusClient
client = CerberusClient('https://my.cerberus.url', token='mytoken')

Refer to the "local development" section at Quick Start if you're having trouble getting a token.

License

Cerberus Management Service is released under the Apache License, Version 2.0

cerberus-python-client's People

Contributors

anners avatar antonio-osorio avatar azec-nike avatar dependabot[bot] avatar dogonthehorizon avatar fieldju avatar james-michael avatar jelluz avatar jiangha4 avatar mayitbeegh avatar melanahammel avatar phixid avatar sdford avatar shawn-sher avatar splintercat avatar tlisonbee avatar towermagi avatar tunderwood avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cerberus-python-client's Issues

Namespace clash

In case this is still maintained, the pypi name (cerberus-python-client) and underlying folder name (cerberus) don't match. This means that installing both cerberus (https://pypi.org/project/Cerberus/) and cerberus-python-client via pip will cause a namespace clash and break both libraries

Bug: paths in role ARNs aren't supported

I believe there is a bug in this client where paths in role ARNs aren't working.

Basically, if you have an instance-profile ARN like: arn:aws:iam::1234567890123:instance-profile/foobar/foo/foo-app

The role ARN needs to be contructed to look like this: arn:aws:iam::1234567890123:role/foobar/foo/foo-app

We had the same issue in the java client and the fix can be found in how we generate role ARNs from the metadata endpoints in this class,
https://github.com/Nike-Inc/cerberus-java-client/blob/master/src/main/java/com/nike/cerberus/client/auth/aws/InstanceRoleVaultCredentialsProvider.java

See method buildIamRoleArns() and the args for that method should be gathered from the two endpoints:

  1. EC2MetadataUtils.getIAMInstanceProfileInfo().instanceProfileArn; e.g. http://169.254.169.254/latest/meta-data/iam/info
  2. EC2MetadataUtils.getIAMSecurityCredentials().keySet(); e.g. http://169.254.169.254/latest/meta-data/iam/security-credentials/

Above handles the pathing edge case as well as another that occurs in CloudFormation where the role name doesn't match the instance profile name.

Client Version HTTP Header

Cerberus client should self report their name and version via HTTP header with all requests to Cerberus. This is a feature request against all Cerberus clients. This feature will be helpful in understanding:

  • What clients are being used in a particular Cerberus environment
  • If a bug exists in a certain client version, is that client still being used by applications

The format for this header is similar to the User-Agent string. In fact, we considered using the User-Agent Header but thought using a custom header will make some reporting easier.

Specification:
HTTP Header name: 'X-Cerberus-Client'
Example value: 'CerberusJavaClient/1.4.0'

IAM Role vs. Instance Profile Authentication

Two actions required:

  • Update to v2/iam-principal auth
  • Authenticate with Cerberus using the IAM role (instead of the Instance Profile)

Currently the python client uses the EC2 Instance Profile name when authenticating with Cerberus:
https://github.com/Nike-Inc/cerberus-python-client/blob/master/cerberus/aws_auth.py#L61

This is problematic because the real entity that Cerberus needs is the IAM role name. Usually these two names are the same in which case there isn't an issue. However, when the Instance Profile has a separate name (e.g. when generated by CloudFormation) it becomes impossible for Cerberus to find the IAM role that the given Instance Profile is associated with.

The IAM role name can be retrieved from EC2 metadata via this URL:
http://169.254.169.254/latest/meta-data/iam/security-credentials/. Also documented here:
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html

The account ID will still need to be retrieved from the Instance Profile ARN.

** Note: The upgrade to v2 auth is not required, but strongly suggested.

Deprecate CerberusClient.get_secret(vault_path, key) method

The method CerberusClient.get_secret(vault_path, key) is ineffective.
It should either hit API that effectively fetches one SDB value, or should be deprecated.
Current implementation encourages bad practice. It misleads users to naively believe that they are getting only one value, where it actually fetches entire bucket and then filters values by key in the client. In the end this can create unnecessary load on Cerberus APIs.

Method CerberusClient.get_secrets(vault_path) should return SDB keys directly

Current call to CerberusClient.get_secrets(vault_path) returns data in a Python dictionary in which values are not directly accessible by SDB key. As a user I have to get SDB values by first fetching dictionary element with data key and then index by SDB key.
For example, user has to do:

client = CerberusClient("https://prod.cerberus.nikecloud.com")
sdb_path = "some/cerberus/path"
sdb_key = "some_key"
bucket_secrets = client.get_secrets(sdb_path)
#Important line
sdb_secret_of_interest = bucket_secrets['data'][sdb_key]

where they would expect:

client = CerberusClient("https://prod.cerberus.nikecloud.com")
sdb_path = "some/cerberus/path"
sdb_key = "some_key"
bucket_secrets = client.get_secrets(sdb_path)
#Important line - notice change to previous block
sdb_secret_of_interest = bucket_secrets[sdb_key]

This behavior (if implemented) is a breaking change.

Client fails for lambdas if no role_arn to be assumed is set

It looks like if I don't set a role_arn to assume (just want to use the Lambda's execution role), then this logic assumes that an ec2 instance is calling it, so will fail for Lambdas.

instance_profile_arn = requests.get('http://169.254.169.254/latest/meta-data/iam/info').json()['InstanceProfileArn']

Example error I'm seeing from Lambda:

module initialization error: HTTPConnectionPool(host='169.254.169.254', port=80): 
Max retries exceeded with url: /latest/meta-data/iam/info 
(Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f47c888a7f0>:
 Failed to establish a new connection: [Errno 111] Connection refused',))

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.