A3S
NOTE: This is a work in progress.
A3S (stands for Auth As A Service) is an authentication and ABAC authorization
server.
It allows to normalize various sources of authentication like OIDC,
AWS/Azure/GCP tokens, LDAP and more into a generic identity token that contains
identity claims (rather than scopes). These claims can be used by some
authorization policies to give a particular subset of bearers various
permissions.
These authorization policies match a set of bearers based on a logical claims
expression (like group=red and color=blue or group=admin
) and they apply to a
namespace.
A namespace is a node that is part of hierarchical tree that represents an
abstract organizational unit. The root namespace is named /
.
Basically, an authorization policy allows a subset of bearers, defined by claims
retrieved from an authentication source, to perform actions in a particular
namespace and all of its children.
Apps can receive a request alongside a delivered identity token then check with
A3S if the current bearer is allowed to perform a particular action in a
particular namespace.
Table of contents
Quick start
The easiest way to get started is to use the docker-compose.yaml
in the dev
folder.
First, install the tools needed:
go install go.aporeto.io/tg@master
go install go.aporeto.io/elemental/cmd/elegen@master
go install go.aporeto.io/regolithe/cmd/rego@master
go install github.com/aporeto-inc/go-bindata/go-bindata@master
Then generate the needed certificates:
dev/certs-init
This creates the certificates in dev/.data/certificates
that the A3S container
will mount (the same certificates will be used by the dev environment, described
later).
Then build the docker container:
make docker
And finally start the docker-compose file:
cd ./dev
docker compose up
Using the system
You can start to interact with the system by using the raw API with curl or
using the provided cli named a3sctl
. The later provide a streamlined interface
that makes it more pleasant to use than the raw API.
Install a3sctl
To install the cli, run:
make cli
This will install a3sctl
into you$GOBIN
folder. You should have this folder
in your $PATH
if you want to use the cli without needing to enter its full
path.
Obtain a root token
In order to configure the system and create additional namespaces,
authorizations, etc., you need to obtain a root token to start interacting with
the server:
a3sctl auth mtls \
--api https://127.0.0.1:44443 \
--api-skip-verify \
--cert dev/.data/certificates/user-cert.pem \
--key dev/.data/certificates/user-key.pem \
--source-name root
NOTE: In production environment, never use --api-skip-verify. You should
instead trust the CA used to issue A3S TLS certificate.
This will print a token that you can use for subsequent calls. You can set the
$A3SCTL_TOKEN
environment variable to use it automatically.
NOTE: There are easier ways to deal with retrieving a token when using a3sctl
as it will be explained later.
If you want to check the content of a token, you can use:
$ a3sctl auth check --token <token>
alg: ES256
kid: 1DAA6949AACB82DBEF1CFE7D93586DD0BF1F090A
{
"exp": 1636830341,
"iat": 1636743941,
"identity": [
"@source:name=root"
"@source:namespace=/",
"@source:type=mtls",
"commonname=Jean-Michel",
"fingerprint=C8BB0E5FA7644DDC97FD54AEF09053E880EDA939",
"issuerchain=D98F838F491542CC238275763AA06B7DC949737D",
"serialnumber=219959457279438724775594138274989969558",
],
"iss": "https://127.0.0.1",
"jti": "b2b441a0-5283-4586-baa7-4a45147aaf46"
}
You can omit --token
if you have set $A3SCTL_TOKEN
. If you need to print the
raw token, you can use the --print
flag.
Test with the sample app
There is a very small python Flask server located in /example/python/testapp
.
It comes with a script that creates a namespace, an MTLS source and two
authorizations that will be used to demo a basic use of A3S.
You can take a look at the README in that
folder to get started.
Obtaining identity tokens
This section describes how to create the various sources of authentication, how
to retrieve a token from them, apply restrictions or apply cloaking.
All the following examples will assume to work in the namespace /tutorial
. To
create it, you can run:
export A3SCTL_API="https://127.0.0.1:44443"
export A3SCTL_API_SKIP_VERIFY="true"
export A3SCTL_NAMESPACE=/tutorial
a3sctl api create namespace --with.name tutorial --namespace /
NOTE: The env variable will tell a3sctl which namespace to target without
having to pass the --namespace
flag every time.
NOTE: Some auth commands will require to pass the namespace of the auth
source. You can either set --source-namespace
or leave it empty to fallback
on the value set by --namespace
.
NOTE: You can also get more info about a resource by using the -h
flag.
This will list all the possible properties the API supports.
Restrictions
Whichever authentication source you are using, you can always ask for a
restricted token. A restricted token contains additional user requested
restrictions preventing actions that would normally be possible to do based on
the authorizations stored in the server, matching the bearer claims.
--restrict-namespace
: a namespace restricted token will only be valid if
used on the restricted namespace or one of its children.
--restrict-network
: a network restricted token can only be used if the
source network from which it is used is contained in one of the restricted
networks.
--restrict-permissions
: limits what permissions the token will have. For
instance if your authorization set grants dog:eat,sleep
, you may ask for a
token that will only work for dog:eat
.
Cloaking
It is possible to limit the amount of identity claims that will be embedded into
the identity token by using the --cloak
flag. This can be useful for privacy
reasons. For instance, if a party requests you to have color=blue
and this is
the only claim that matters, you can hide the rest of your claims by passing
--cloak color=blue
Cloaking uses prefix matching. So you can decide to only embed the color and
size claims (if you have multiple of them) by doing:
--cloak color= --cloak size=
Identity modifiers
Certain authentication sources allow to set an additional identity modifier.
This is an URL to an HTTPS server running on your own premises, that will be be
called by A3S when it is about to deliver a token from the source.
One reason one may want to do so is to enhance claims based on an external
system. We can imagine an A3S server run by a health care provider, that may
trust a more global A3S instance. This instance could return a claim based the
bearer's SSN and the health care provider may want to deliver a token that would
contain additional information, like a blood type for instance. The identity
modifier would then query an external database to match the blood type on record
with the bearer SSN.
The server will receive the claims that are about to be delivered, and will
have a chance to modify the list. The server must implement MTLS authentication
and must accept the certificates set in the source modifier.
The server must return 200
if it did modify the claims or 204
if it did not.
Any other code will be treated as an error.
The source modifier allows to set the HTTP method to use when calling the remote
server. If it is POST
, PUT
or PATCH
, A3S will send the claims as a JSON
array encoded in the body. For GET
, the server will set the claims in the
query parameter claim
.
In any case, the server will receive the following headers, describing the
source that was used to derive the identity claims:
x-a3s-source-type
x-a3s-source-namespace
x-a3s-source-name
The server must return the list of modified claims as a JSON-encoded array in
the body of the response.
A server must not insert any identity claims starting with the symbol @
or A3S
will refuse to deliver the token.
NOTE: You can find a naive implementation of a claim modifier in
examples/python/claimmod
. You can take a look at the
README in that folder.
Authentication sources
While A3S allows to verify the identity of a token bearer, it does not provide
any way to store information about the user. In order to derive identity
claims, A3S relies on third-party authentication sources, who hold the actual
data about a bearer.
MTLS
The MTLS source uses mutual TLS to authenticate a client. The client must present
a client certificate (usage set to auth client) that is signed by the CA
provided in the designed MTLS auth source.
NOTE: This authentication source supports identity modifiers.
Create an MTLS source
You first need to have a CA that can issue certificates for your users. In this
example, we use tg
, but you can use any PKI tool you like.
tg cert --name myca --is-ca
tg cert --name user1 \
--signing-cert myca-cert.pem \
--signing-cert-key myca-key.pem
NOTE: tg can be installed by running go install go.aporeto.io/tg@master
NOTE: Not protecting a private key with a passphrase is bad. Don't do this in
production.
Then we need to create the MTLS auth source:
a3sctl api create mtlssource \
--with.name my-mtls-source \
--with.ca "$(cat myca-cert.pem)"
Obtain a token from MTLS source
To obtain a token from the newly created source:
a3sctl auth mtls \
--source-name my-mtls-source \
--source-namespace /tutorial \
--cert user1-cert.pem \
--key user1-key.pem
If the private key is encrypted, you need to set the flag --pass <passphrase>
.
NOTE: You can set -
for --pass
. In that case, a3sctl will ask for user
input from stdin.
LDAP
A3S supports using a remote LDAP as authentication source. The LDAP server must
be accessible from A3S. A3S will refuse to connect to an LDAP with no form of
encryption (TLS or STARTTLS).
NOTE: This authentication source supports identity modifiers.
Create an LDAP source
To create an LDAP source, run:
a3sctl api create ldapsource \
--with.name my-ldap-source \
--with.address 127.0.0.1:389 \
--with.certificate-authority "$(cat ldap-ce-cert.pem)" \
--with.base-dn dc=universe,dc=io \
--with.bind-dn cn=readonly,dc=universe,dc=io \
--with.bind-password password
- The
base-dn
is the DN to use to search for users.
- Yhe
bind-dn
is the account A3S will use to connect to the ldap. It should be
a readonly account.
- The
bind-password
is the password associated to the bind-dn
.
You can also use --certificate-auhority
to pass a custom CA if the
certificates used by the server are not trusted by the host running A3S.
You can decide to ignore certain attribute by using the flag --ignore-keys
.
The opposite way is also posible by using --include-keys
to only include the
desised attributes. If the same attribute is set in both flag, it will end up
being ignored.
Obtain a token from LDAP source
To obtain a token from the newly created source:
a3sctl auth ldap \
--source-name my-ldap-source \
--namespace /tutorial \
--user bob \
--pass s3cr3t
NOTE: You can set -
for --user
and/or --pass
. In that case, a3sctl will
ask for user input from stdin.
HTTP
A3S supports using a remote HTTP server as authentication source. The HTTP
server must be accessible from A3S. A3S will refuse to connect if the server
does not support MTLS. This can be used to link to your own internal account
system.
When an HTTP source is used, A3S will send a POST request to the corresponding
server containing a JSON-encoded map with the following items:
username
: the user provided user name.
password
: the user provided password.
TOTP
: optional one-time password for 2FA.
The server must respond 200
with a body containing the claims to insert in the
token as JSON-encoded array (for instance: ["username=bob", "bu=eng"]
). Any
other status code will be returned as an 401
error to the user.
NOTE: This authentication source supports identity modifiers.
Create an HTTP source
To create an HTTP source, run:
a3sctl api create httpsource \
--with.name my-http-source \
--with.url https://myserver.com/login \
--with.certificate-authority "$(cat ca-cert.pem)" \
--with.certificate "$(cat client-cert.pem)" \
--with.key "$(cat client-key.pem)"
Obtain a token from HTTP source
To obtain a token from the newly created source:
a3sctl auth http \
--source-name my-http-source \
--namespace /tutorial \
--user bob \
--pass s3cr3t \
--totp 1234
NOTE: You can set -
for --user
and/or --pass
. In that case, a3sctl will
ask for user input from stdin.
OIDC
A3S can retrieve an identity token from an existing OIDC provider in order to
deliver normalized identiy tokens.
NOTE: This authentication source supports identity modifiers.
Create an OIDC source
Configuring a valid OIDC provider is beyond the scope of this document. However,
they will all work the same and will provide you with a client ID, a client
secret and an endpoint.
It is however important to allow http://localhost:65333
as a redirect URL from
your provider confguration if you plan to use a3sctl to authenticate.
Once the provider is configuired, create an OIDC source:
a3sctl api create oidcsource \
--with.name my-oidc-source \
--with.client-id <client id> \
--with.client-secret <client secret> \
--with.endpoint https://accounts.google.com \
--with.scopes '["email", "given_name"]'
The scopes indicate the OIDC provider which claims to return. They will vary
depending on your provider.
You can also use --certificate-auhority
to pass a custom CA if the
certificates used by the OIDC providers are not trusted by the host running A3S.
Obtain a token from OIDC source
While all the other sources can be used easily with curl for instance, the OIDC
source needs to run an HTTP server and needs to perform a dance that is quite
painful to do manually. a3sctl will do all of this transparently.
To obtain a token from the newly created source:
a3sctl auth oidc \
--source-name my-oidc-source \
--source-namespace /tutorial
This will print a URL to open in your browser to authenticate against the OIDC
provider. Once completed, the provider will reply and the token will be
displayed.
A3S remote identity token
This authentication source allows to issue a token from another one issued by
another A3S server. It allows to trust other A3S instances and issue local
tokens from trusted ones, while potentially augmenting the identity claims.
NOTE: This authentication source supports identity modifiers.
Create an A3S source
You need to create an A3S source in order to validate the remote tokens. The
source requires to pass the raw address of the remote A3S server, as it will use
the well-known JWKS URL to retrieve the keys and verify the token signature.
To create an A3S source:
a3sctl api create a3ssource \
--with.name my-remote-a3s-source \
--with.issuer https://remote-a3s.com
You can also use --certificate-auhority
to pass a custom CA if the
certificates used by the server are not trusted by the host running A3S.
If the issuer is not identical to the root URL of the remote A3S server, you can
use the --with.endpoint
flag to pass the actual URL.
Obtain a token from A3S source
To obtain a token from the newly created source:
a3sctl auth remote-a3s \
--source-name my-remote-a3s-source \
--source-namespace /tutorial \
--input-token <token>
Amazon STS
This authentication source does not need custom source creation as it uses AWS
broadly. How to retrieve a token from AWS is beyond the scope of this document.
However, if you run a3sctl from an EC2 instance that has an IAM role assigned,
it will retrieve one for you, if you don't pass any additional information.
If you are not running the command on AWS:
a3sctl auth aws \
--access-key-id <kid> \
--access-key-secret <secret> \
--access-token <token>
If you are running it from an AWS EC2 instance, you just need to run:
a3sctl auth aws
This authentication source does not need custom source creation as it uses GCP
broadly. How to retrieve a token from GCP is beyond the scope of this document.
However, if you run a3sctl from a GCP instance, it will retrieve one for you, if
you don't pass any additional information
If you are not running the command on GCP:
a3sctl auth gcp --access-token <token>
If you are running it from an GCP instance, you just need to run:
a3sctl auth gcp
Azure token
This authentication source does not need custom source creation as it uses Azure
broadly. How to retrieve a token from Azure is beyond the scope of this
document. However, if you run a3sctl from an Azure instance, it will retrieve
one for you, if you don't pass any additional information
If you are not running the command on Azure:
a3sctl auth azure --access-token <token>
If you are running it from an Azure instance, you just need to run:
a3sctl auth azure
A3S local identity token
You can use an existing A3S identity token to ask for another one. Note that is
not a renew mechanism. The requested token cannot expire later than the original
one. The goal of this authentication source is to ask for a more restricted
and/or cloaked version of the original.
This authentication source does not need custom source creation.
To get obtain a token:
a3sctl auth a3s --token <token> \
--restrict-namespace /a/child/ns \
--restrict-network 10.0.1.1/32 \
--restrict-permissions "dog:eat,sleep"
Writing authorizations
The Authorizations allows to match a set of users (subjects) based on a claim
expression and assign them permissions. Authorizations work on a whitelist model.
Everything that is not explicitely allowed is forbidden.
Subject
A matching expression can be described as a basic boolean sequence like
(org=acme && group=finance) || group=admin
. They are represented by a
two-dimensional array. As such, the expression above is written:
[
[ "org=admin", "group=finance" ],
[ "group=admin" ]
]
The first dimension represents or
clauses and the second represents and
clauses.
As there are many sources of authorization and delivered claims can overlap,
potentially given way broader permissions than expected, the identity token
always contains additional claims allowing to discriminate bearers based on the
authentication source they used.
@source:type
: The type of source that was used to deliver the token.
@source:namespace
: The namespace of the source that was used.
@source:name
: The name of the source.
NOTE: Claims starting with the symbol @
are reserved. If an authentication
source tries to insert such claims, all prefixing @
will be removed.
This way, you can differentiate name=bob
based on which Bob we are aiming. A
safe subject to use in that case:
[
["@source:type=ldap", "@source:namespace=/my/ns", "name=bob"]
]
The authorization will only match Bob who got a token from any LDAP
authentication source that has been declared in /my/ns
. Another Bob from
another namespace or coming from an OIDC source will not match.
Permissions
Authorizations also contain a set of permissions that describes what the
matching bearers can do. They are generic (ie they don't make assumptions about
the underlying protocol you are using) and are represented by a string of the
form:
"resource:action1,...,actionN[:id2,...idN]"
For instance, the following allows the bearer to walk and pet the dogs:
"dogs:pet,walk"
The following allows the bearer to GET /admin:
"/admin:get"
The following allows to GET and PUT authorizations with ID 1 or 2:
"authorizations:get,put:1,2"
Permissions can use the *
as resource or actions to match any. As such, the
following permission gives the bearer admin access:
"*:*"
An authorization contains an array of permissions, granting the bearer the union
of them. If multiple authorizations match the bearer identity token, then the
union of all their permissions will be granted.
Target namespaces
An authorization lives in a nanmespace and can target the current namespace of
some of their children. Authorizations propagate down the namespace hierarchy
starting from where it applied. It can not affect parents or sibling namespaces.
Examples
We can create the authorization described above with the following command:
a3sctl api create authorization
--namespace /my/namespace \
--with.name my-auth \
--with.target-namespaces '["/my/namespace/app1"]' \
--with.subject '[
[
"@source:type=oidc",
"@source:namespace=/my/namespace",
"org=admin",
"group=finance",
],
[
"@source:type=mtls",
"@source:namespace=/my",
"@source:name=admins",
"group=admin",
]
]' \
--with.permissions '["dogs:pet,walk"]'
NOTE: If you omit --target-namespace
, then the authorization applies to its
own namespace and children.
Check for permissions from your app
A3S provides an API to verify if a token bearer is allowed to performed some
actions. The easiest way to implement this is to add an authentication
middleware in whatever HTTP framework you are using to call A3S to verify a
token and its permissions. This middleware can call the all-in-one check
endpoint /authz
. The following example uses curl, but you should use the HTTP
communication layer currently used in your application.
curl -H "Content-Type: application/json" \
-d '{
"token": <token>,
"resource": "/dogs"
"action": "walk",
"namespace: /application/namespace",
"audience": "my-app",
}' \
https://127.0.0.1:44443/authz
This would return 204
if the bearer is allowed to walk the dogs in
/application/namespace
, or 403
if either the token is invalid or the bearer
is not allowed to perform such action.
This method is the simplest but has a few drawbacks. For instance, you will
make A3S validate the token everytime, you need to make a call everytime, and
you need to transmit the bearer token at every call.
A more optimized method will be described here soon, that allows to:
- Validate token signature yourself locally
- Retrieve the entire permissions set for a given token for caching
- Validate the permissions locally
- Be notified when cached permissions needs to be invalidated.
NOTE: This method requires the third-party application to be able to connect
to the push channel, and hence will require to be authenticated.
Using a3sctl
a3sctl is the command line that allows to use A3S APIs in a user-friendly manner.
It abstracts the ReST APIs and is self-documenting. You can always get additional
help by passing the flags --help
(or -h
) in any command or subcommand.
Completion
a3sctl supports auto-completion:
Bash
. <(a3sctl completion bash)
Zsh
compdef _a3sctl a3sctl
. <(a3sctl completion zsh)
Fish
. <(a3sctl completion fish)
Configuration file
a3sctl can read the values of its flags from various places, in the following order:
- A flag directly provided, or
- Env variable (ie
$A3SCTL_SOURCE_NAME
for --source-name
), or
- The config file (default:
~/.config/a3sctl/default.yaml
)
You can choose the config file to use by setting the full path of the file
using the flag --config
(or $A3SCTL_CONFIG
).
You can also pass the name of the config, without its folder or its extension
through the flag --config-name
(or $A3SCTL_CONFIG_NAME
). a3sctl will scan
the following folders, in the below order, to find a configuration file matching
the name:
~/.config/a3sctl/
, or
/usr/local/etc/a3sctl/
, or
/etc/a3sctl/
Auto-authentication
In addition to one-to-one mapping of a3sctl flags in the config file, you can
also add the key autoauth
to automatically retrieve, cache, reuse and renew a
token using a particular authentication source. This method works for MTLS and
LDAP.
For instance, in ~/.config/a3sctl/default.yaml
:
api: https://127.0.0.1:44443
namespace: /
autoauth:
enable: mtls
ldap:
user: okenobi
pass: '-'
source:
name: root
namespace: /
mtls:
cert: /path/to/user-cert.pem
key: /path/to/user-key.pem
pass: '-'
source:
name: root
namespace: /
You can decide which source to use for auto-authentication by setting the
enable
key. Leave it empty to disable auto-authentication.
The token is cached in $XDG_HOME_CACHE/a3sctl/token-<src>-<api-hash>
and will
automatically renew if it's past its half-life.
NOTE: Using -
for secrets will automatically prompt the user for input during
retrieval or renewal of the token.
Import
A3S allows to manage the content of a namespace through declarative import. At
its core, importing is done using the /import
API.
The following resources can be imported:
- All kind of sources (
oidcsources
, mtlssources
, etc...)
authorizations
An import must provide a label in order to recognize all the resources imported.
This will help the system to realign any changes between the import declarartion
and the current state of the system. You must ensure the provided label is
unique to an import in a namespace, or you may face unintended side effects. The
label will be stored into the importLabel
property of the resources.
Imported resources use a hash (stored in importHash
) to determine if the data
provided in the import request has changed or not. If the data is unmodified,
the import will leave the object in place. If it has been modified, the import
system will delete, then recreate a new version of the object (that means its
ID
will change).
Simple import files with a3sctl
a3sctl provides an easy way to deal with import declarations stored in a YAML
file.
For instance:
label: my-import-label
Authorizations:
- name: authorization-a
subject:
- - "@source:type=mtls"
- "@source:name=default"
- "@source:namespace=/ns"
- "commonname=john"
permissions:
- /resource-a:GET
- name: top-secret-access
subject:
- - "@source:type=mtls"
- "@source:name=default"
- "@source:namespace=/ns"
- "commonname=michael"
permissions:
- /resource-b:GET
- /resource-c:GET
This file declares two authorizations
, under the label my-import-label
.
To import this file, run:
a3sctl import path/to/file.yaml
a3sctl also allows to point to a URL.
For instance:
apoctl import https://server.com/import.yaml
Templating with a3sctl
Import files support go templating with
sprig functions, as well as helm
style
values management.
NOTE: You can check the rendering of the template by passing --render
For instance, consider the following file:
label: my-templated-import:
MTLSSources:
- name: {{ .Values.name }}
description: {{ .Values.desc }}
CA: |-
{{ readFile "path/to/ca.pem" | indent 4 }}
This file can be imported using the following command:
a3sctl import mytemplate.gotmpl \
--set name=hello \
--set "desc=the description"
This will replace {{ .Values.name }}
and {{ .Values.desc }}
by hello
and
the description
respectively. You can also notice the call to readFile
that
will be replaced by the content of the file given as parameter.
In addition to the .Values
dictionary, there are the following additional
values that can be accessed:
{{ .Common.API }}
: the value of the --api flag
{{ .Common.Namespace }}
: The value of --namespace flag
Finally, you can store the values in their own files, and use them to populate a
template. For instance:
name: hello world
description: the description
You can use the values in that file by doing:
a3sctl import mytemplate.gotmpl --values myvalues.yaml
Development environment
Prerequesites
First, clone this repository and make sure you have the following installed:
- go
- mongodb
- nats-server
- tmux & tmuxinator
Initialize the environment
If this is the first time you start the environment, you need to initialize
various things.
First, initialize the needed certificates:
dev/certs-init
Then initialize the database:
dev/mongo-init
All the development data stored stored in dev/.data
. If you delete this
folder, you can reinitialize the environment.
All of A3S configuration is defined as env variables from dev/env
Finally, you must initialize the root permissions. A3S makes no exceptions nor
has any hardcoded or weak credentials, so you must add an authentication source
and an authorization.
To do so, run:
dev/a3s-run --init --init-root-ca dev/.data/certificates/ca-acme-cert.pem
NOTE: Even if you are in the dev
folder the root CA must be passed relative
to the root of the repository.
Start everything
Once initialized, start the tmux session by running:
dev/env-run
This will launch a tmux session starting everything and giving you a working
terminal. To exit:
env-kill
Support
Please read SUPPORT.md for details on how to get support for this
project.
Contributing
We value your contributions! Please read CONTRIBUTING.md
for details on how to contribute and the process for submitting pull requests
to us.