Adding OpenStack Keystone authentication to a Flask application

Published by Taavi Väänänen on January 1, 2022.

The Wikimedia Cloud VPS platform is powered by the OpenStack platform. In addition to the standard OpenStack services, we've needed to build our own services and APIs that add features specific to our installation.

Historically our OpenStack APIs have not been open to direct use and we've required everyone to use the OpenStack dashboard (Horizon) to manage their VPS project. Our custom APIs have been "secured" by just firewalling access from everywhere else except the OpenStack control hosts, which has worked fine for us in the past. However, we are planning on letting our users directly access the APIs to build their own integrations, and as a part of this work we needed to replace the firewall-based security model with something more flexible. The natural solution is to integrate our custom APIs with OpenStack's Identity service (Keystone).

Our services use the Flask framework. Thankfully this means that we can just use the code that Rackspace has written: the flask_keystone package does exactly what we want. The documentation is a bit lacking though.

flask_keystone makes heavy use of OpenStack's shared library Oslo and uses Rackspace's flask_oslolog too, so you need to install them. At Wikimedia we built our own Debian packages for both Rackspace libraries (Oslo is already packaged for on the official Debian repositories).

A minimal code example looks something like this:

import flask

from flask_keystone import FlaskKeystone
from flask_oslolog import OsloLog
from oslo_config import cfg
from oslo_context import context

key = FlaskKeystone()
log = OsloLog()

cfg.CONF(default_config_files=['/etc/path-to-your/config.ini'])

app = flask.Flask(__name__)

log.init_app(app)
key.init_app(app)


def get_oslo_context(rule, project_id):
    # headers in a specific format that oslo.context wants
    headers = {
        f'HTTP_{name.upper().replace("-", "_")}': value
        for name, value in flask.request.headers.items()
    }

    return context.RequestContext.from_environ(headers)


@app.route("/")
def hello():
    ctx = get_oslo_context()
    return f'Hello, {ctx.user_id} on project {ctx.project_id}!'

if __name__ == '__main__':
    app.run()

You will also need a config file in the path you gave to oslo_config. A sample configuration looks something like this:

[DEFAULT]
auth_strategy=keystone

[keystone_authtoken]
identity_uri = https://openstack.example.org:25000
www_authenticate_uri = https://openstack.example.org:25000

http_connect_timeout = 5

interface = public
auth_version = 3.14
auth_type = password
auth_url = https://openstack.example.org:25000
user_domain_id = default
project_domain_id = default

username = some-username
password = some-password
project_name = some-project

service_type = some-keystone-service-type

delay_auth_decision = True

[flask_keystone]
roles = ""

In the config file, you need to add credentials for an OpenStack account that has access to the identity:validate_token rule. We added a new role for that purpose and granted our service account that role domain-wide.

Authenticating your requests

Now that your service is secured with Keystone authentication, you probably want to make some requests to it. This can be done using the keystoneclient python package:


from keystoneauth1 import session as keystone_session
from keystoneauth1.identity import v3
from keystoneclient.v3 import client

auth = v3.Password(
    auth_url='https://openstack.example.org:25000',
    username='some-username',
    password='some-password',
    project_id='some-project',
    user_domain_name='default',
    project_domain_name='default',
)

session = keystone_session.Session(
    auth=auth,
    user_agent='some-app',
)

client = client.Client(
    session=session(),
    interface='public',
    timeout=5,
)

service = client.services.list(type='some-keystone-service-type')[0]
endpoint = client.endpoints.list(service=service.id, interface='public', enabled=True)[0]
url = endpoint.url

print(session.get(f'{url}/test').json())

Policy-based access control

We can also use OpenStack oslo.policy for policy-based access control, similar to other OpenStack services.

First, create an Enforcer object for your policy rules:

from oslo_config import cfg
from oslo_policy import policy

# you should already have this
cfg.CONF(default_config_files=['/etc/path-to-your/config.ini'])

enforcer = policy.Enforcer(cfg.CONF)
enforcer.register_defaults([
    policy.RuleDefault('admin', 'role:admin'),
    policy.RuleDefault('proxy:index', ''),
    policy.RuleDefault('proxy:view', ''),
    policy.RuleDefault('proxy:create', 'rule:admin'),
    policy.RuleDefault('proxy:update', 'rule:admin'),
    policy.RuleDefault('proxy:delete', 'rule:admin'),
])

Then you can enforce the policies in your route handlers:

@app.route('/v1/proxy')
def all_proxies():
    ctx = get_oslo_context()
    result = enforcer.authorize('proxy:index', {}, ctx)

    if not result:
        return flask.json({'error': 'forbidden'}), 403

    return flask.json(get_proxies(ctx.project_id))

This entry is tagged as: openstack project wikimedia

Feedback? Please email any comments to hi@taavi.wtf, or toot them at @taavi@wikis.world.