This is an internal gateway used to authenticate Red Hat associates against internal Red Hat SSO or mTLS, along with an auth/policy service to handle SAML authentication and provide ACL-based authorization.
Turnpike makes use of the auth_request feature of Nginx, which delegates authentication/authorization to a web service. The web service itself is a Flask application that implements a SAML Service Provider and allows/denies specific requests to routed web applications & APIs based on the attributes of the SAML assertion. Session data is stored in Redis to better manage content and expiry.
A successful SAML user request workflow looks like:
A successful mTLS request workflow looks like:
Note: TLS can be terminated either with Nginx itself, or with a standalone service in front of Nginx. The diagram's
"mTLS Gateway" refers to whatever service terminates TLS. Also, the expected subject and issuer header names can be
adjusted by setting HEADER_CERTAUTH_SUBJECT
and HEADER_CERTAUTH_ISSUER
variables.
You can use scripts/setup_devel_env
to configure the certs and configure to use the samltest.id IdP. Once the app is
running, you will need to visit https://samltest.id/upload.php and upload https://turnpike.example.com/saml/metadata.xml
before the IdP will recognize your turnpike instance. The SP entityId includes a uuid so that you don't accidentally
overwrite another developer's SP XML.
If you want to configure manually or understand what the script does, read on.
In services/nginx
, create the following files:
certs/cert.pem
- The PEM encoded certificate for Nginx (can be the same as the web cert)certs/key.pem
- The PEM encoded certificate for Nginx (can be the same as the web key)certs/ca.pem
- The PEM encoded trust chain to verify client certificates
The implementation of a SAML service provider uses the OneLogin python3-saml library.
As with any SAML SP configuration, you will need:
- An SSL certificate and key for your Turnpike gateway
- The SSL certificate for your SAML Identity Provider (IdP)
- The configuration of your Identity Provider as exposed by their IdP metadata endpoint.
Then in your copy of the Turnpike code, in /saml
create the following files:
settings.json
- The metadata settings for the IdP and SP (an example)advanced_settings.json
- Advanced SAML settings for the IdP and SP (an example)
You can fill out the latter two by finding corresponding fields in your IdP's metadata file and with collaboration with their staff. For the Service Provider urls, you should use the following paths relative to your Turnpike hostname:
- Entity ID/metadata URL:
/saml/metadata.xml
- ACS:
/saml/acs/
- SLS:
/saml/sls/
If you're looking to simply demo Turnpike or aren't ready to integrate with your real IdP yet, you can use the free SAML test integration services available at https://samltest.id
You will also need to generate a TLS certificate for the hostname you're going to use to access your running development
environment. In /nginx/certs
, name the certificate as cert.pem
and the private key as key.pem
.
If you are deploying Turnpike in Kubernetes or OpenShift, you should mount these configuration files using a combination of ConfigMap and Secret resources mounted into your running pods.
The simplest way to run Turnpike locally is using Docker Compose.
First, you need to set proper environment variables. Copy the .env.example
file to .env
, copy the
dev-config.py.example
to dev-config.py
, and customize them both as needed. You'll
need to generate a secret key for session security and you'll need to set the SERVER_NAME
to the hostname you're
using for your SAML Service Provider, the same as the subject in your SP certificate.
Then, from the root of your copy of Turnpike, simply run:
docker-compose build
docker-compose up
If the subject of the SP certificate does not resolve to your local machine, you may have to add a mapping in your
/etc/hosts
file. For example, if you issued your SP certificate to cn=turnpike.example.com
, then you may need to add
to your /etc/hosts
file a line:
127.0.0.1 turnpike.example.com
Then, go to https://turnpike.example.com/api/turnpike/identity in your browser. You should immediately be redirected to your configured IdP's login page, or if you're already logged into your IdP, a page that outputs the content of your IdP's SAML assertion.
Turnpike expects that in the Flask container it will be able to find its route map and access control list at
/etc/turnpike/backends.yml
. You can override it in app-interface. For Docker Compose, the file in your copy of Turnpike dev-backends.yml
is mounted into
the Flask container at this path:
- name: turnpike
route: /api/turnpike
origin: http://web:5000/api/turnpike
auth:
saml: "True"
- name: healthcheck
route: /_healthcheck
origin: http://web:5000/_healthcheck
The file is a list of routes. Each route has three required fields: a name
which must be a unique string, a route
which represents a URL prefix substring to match for this route, and an origin
which represents the URL to proxy to.
The substring matching of route
and the rewriting to origin
are the same as the Nginx location matching and rewrite
rules.
If a route has a key auth
, then it will require authentication. If the auth
key is missing, the default is to forbid
access unless the route resides under /public/
(see best practices). The auth
key's value should be a set of key/value
pairs representing supported authentication schemes and corresponding authorization rules. At this time, the only
supported authentication schemes are saml
, x509
.
The value associated with saml
should be a Python expression that evaluates to True
or False
. The only variable
in the expression is a dictionary user
which contains the SAML assertion for the requesting user. If the assertion
had multiple AttributeValue
s for a single Attribute
, then those values are represented as a list of values.
Note: The Red Hat SSO SAML assertion will return LDAP roles which Turnpike makes available to your predicates via:
user['Role']
. Production SSO will return your production LDAP groups, configured at https://rover.redhat.com/groups/ while Stage and other non-prod environments will return groups configured at: https://rover.stage.redhat.com/groups/. If you create a new role for cloud.dot purposes, it is a good idea to prefix it with ' crc-'.
So for example, if you wanted to limit access to a route to users who had the role admin
, auditor
, or manager
,
your Python expression could be:
set(['admin', 'auditor', 'manager']).intersection(set(user['roles']))
The evaluation would use set-logic to look for overlaps. If there were any overlaps, the predicate would evaluate to
True
. If not, False
.
x509
also takes a Python expression which evaluates to True
or False
. It is passed a dictionary x509
which
contains two attributes: subject_dn
and issuer_dn
which can be used to further restrict which certs can be used
(nginx already verifies the trust chain based on the configured CA file).
For example to restrict the endpoint to a certificate with the DN of /CN=test
, you could use:
x509['subject_dn'] == '/CN=test'
Note: To check your certificate's subject and issuer, you can use
openssl x509 -in <cert> -noout -subject
and join the content with slash, e.g. 'O=rhds, OU=serviceaccounts, UID=nonprod-hcc-rbac' to '/O=rhds/OU=serviceaccounts/UID=nonprod-hcc-rbac'
If using TLS terminated at Nginx, note that CRL and/or OCSP support should be configured in NGINX_SSL_CONFIG
as
needed.
Turnpike's policy service is based on passing a request through a series of plugins that evaluate the request for conformity with the deployment policies. These plugins allow for a great deal of customization and enhancement with minimal code changes.
The plugin chain is configurable in the Flask application using the PLUGIN_CHAIN
setting, which contains a list of Python references to classes that subclass the
TurnpikePlugin
abstract base class.
The turnpike.plugins.auth.AuthPlugin
has its own special type of sub-plugin that
implements the TurnpikeAuthPlugin
abstract base class. By setting a separate list of
authentication methods as AUTH_PLUGIN_CHAIN
, you can customize the way that your
deployment supports authenticating and authorizing requests.
See the docstrings on those classes for more details.
You can check out the service documentation in the docs/
folder for more info.
Such as how to access turnpike, acquire certs, etc.
To run local unit tests:
$ pytest
For testing SAML functionality, when the Flask config has TESTING
set to a value that
resolves to True
, an additional endpoint exists at /saml/mock/
which, if you POST
a JSON object representing the contents of the SAML assertion you want to test with,
will set the requestor's session to contain that assertion. The response will have a
Set-Cookie
header for the session; simply present that as a Cookie
header in
subsequent requests to use the stored session.
$ curl -vk -H "Content-Type: application/json" -d '{"foo":"bar"}' https://turnpike.example.com/saml/mock/
... snip ...
< HTTP/1.1 204 NO CONTENT
< Server: nginx/1.14.1
< Set-Cookie: session=b56b832d-8aaa-4c06-9e80-3b424f0fcab2; Domain=.turnpike.example.com; Expires=Thu, 24-Sep-2020 22:05:43 GMT; Secure; HttpOnly; Path=/
<
$ curl -k -H "Cookie: session=b56b832d-8aaa-4c06-9e80-3b424f0fcab2" https://turnpike.example.com/api/turnpike/identity/
{"identity":{"associate":{"foo":"bar"},"auth_type":"saml-auth","type":"Associate"}}
This endpoint returns a 404 if TESTING
is not enabled.
For SAML requests to your API outside of your browser via CURL for instance, you'll
need to supply the session
cookie like so:
curl https://internal.console.redhat.com/api/turnpike/identity/ -b 'session=<SESSION_COOKIE_STRING>'
You can either retrieve this from the browser developer tools by looking for the
session
cookie value, or by accessing https://internal.console.redhat.com/api/turnpike/session/
in the browser, and getting the value from the session
key.
Linting will run automatically with black
in a pre-commit hook, but you'll need to run pre-commit install
first.
You can also run it manually with pre-commit run -a
.