Building a platform is all well and good, but it’s no use if only the Boss Man can use it.
How do we set things up so that other people can actually access the services in the platform?
This is such an obvious question that it has gone overlooked in Hashi@Home until now, because I am the only one using the platform!
Specific tokens issued to me by Vault, Nomad and Consul have been enough for me to access the services and be productive with the platform itself, but this is not acceptable in a scenario where there are (shock!) several users with different roles and permissions.
The context to remember is that we are doing this in order to allow customers to access our platform – access must be governed according to known policies, and be self-service.
This article is a small design study to with some practical considerations on how to allow our first users (platform operators) to access platform services.
We start with a discussion on the architecture and background, then show how the relevant tooling can be deployed in various scenarios to satisfy our initial goals1:
Operators should be able to access platform services with their own credentials
Requirements and Features
To be a bit more formal, instead of expressing a goal, we express a requirement with some features.
The requirement becomes an “acceptance requirement” (we can accept the situation once some statements become true), which in turn express features that we can test.
Requirement:An Operator should be granted access to Platform Services based on their own credentials
Feature: Operators can use their home credentials to access a Platform Service
Scenario: Platform operator in platform owner role can access Vault with admin policy
Given I am user with owner role in assigned in their identity provider
When I log into the Vault web interface via the OIDC provider
Then The OIDC provider redirects me to my identity provider
Then I am asked to authenticate with my personal credentials
And The OIDC provider authorises my access to the service
The task then is to enable this feature for our platform.
After a brief digression into the definition of terms, we’ll design an implementation of the identity architecture that can be deployed in various scenarios to provide it.
Although we’ll go a bit into the assumptions and standards (OAuth and OIDC – see later) we want to adopt, the full-blown details of how we exploit those for wider adoption for subsequent discussion.
Identity architecture
Let’s start by setting the stage for what we mean by some commonly-loaded terms.
When we say “identity”, we usually mean
a digital representation of a person which can be used to authenticate a human
Authentication is the means of providing a proof of your actual identity to a computer, but it is often accompanied by the natural next step: authorisation2.
Authorisation is the means of granting permission to a human to access a given service, and when that access is granted, deciding on what level.
Another term which is used to describe the “level” of permission is the “role” that the human identity assumes inside that service.
Put together, these two terms “authentication” and “authorisation” are often referred to as “AuthN/Z”, such is the frequency with which these concepts coexist.
In the ancient past3, each service had an internal representation of its users, and therefore contained the identities as well as the policies for authorisation.
One of the many downsides to that approach was that identities are duplicated across all of the services which the user wants to use4.
In the enterprisey olden days5, a separation of concerns was introduced where an identity provider was introduced to the picture to act as a central source of truth for identities, but applications still contained their own authorisation engines internally.
In this scenario, although the identities – and thus the user’s credentials (i.e. passwords) – were centrally managed and duplication of identities was addressed, each service still needed to be configured individually.
The generalisation of this this approach gave us the Open ID standard, which provides a way to reliably exchange identities in a decentralised manner.
The duplication in user identities was addressed, but there was still sprawl and lack of governance and compliance.
Subsequently, the OAuth standard was developed, providing an authorisation layer to the identity infrastructure.
To make an long and complicated story considerably shorter6, the combination of these standards into identity providers and authorisation frameworks allows is to start speaking of “AAI”7:
“Authentication and Authorisation Infrastructure”: AAI
The set of standards and tools which permit the decentralisation of identities and access policies, permitting service providers and identity providers to independently manage their data.
The infrastructure which permits access to services based on policies defined by services
We are finally close to what we want for our platform - a thing that can define policies, as well as connect identities to services with permissions defined by those policies.
We will be using Keycloak to implement this authorisation and authentication layer, finally giving us the ability to permit access to platform services without having to also manage the identities of our platform users.
We are going to call this the “Identity Architecture”.
Implementation of Identity Architecture
Now that we have a working definition identity architecture, let’s start working on an implementation as actual services with specific technologies.
The context of the platform identity service is shown in the diagram below, where it is designated “Authentication and Authorisation Service”.
This service starts to provide the “Access” feature we described above in the introduction.
Feature: Access – Authentication and Authorisation workflows
Recall the scenario we initially described as an acceptance criterion?
In that context, someone wishing to access one of the Platform Services8 is redirected to the AAI, which then requests authentication of the user at their organisation’s identity provider (IdP).
After successful authentication, the AAI looks up what the user’s attributes as defined in the authorisation realm, and then passes those to the service which the user initially wanted to use.
The “platform service” we’re using in this acceptance requirement is the Platform Secrets engine, Hashicorp Vault.
However, bear in mind that this requirement can and should be extended to all the other platform services (orchestrator/Nomad, service mesh/Consul, observability/Clickhouse, etc.)
The service’s policies then map those attributes to permissions and roles in its context, and authorises the user to access it with those same permissions and roles.
This is shown in the sequence diagram below:
As the platform owners, we need to deploy the AAI service, synonymous with “Platform Identity”.
This means we are responsible for issuing standard-based attributes to authenticated identities, so that services can decide what permissions and roles to assign to those identities.
These abstract services take the following form in our platform:
The Keycloak service will serve as the Identity service as part of the Platform Security Plane – let’s remind ourselves of the overall design as of summer 20269:
Keycloak sits in the ID management component of the Security Plane.
The external identity store (The LDAP directory of identities) is not shown, because indeed it is not part of the platform, but actually owned by the organisation which is the customer of the platform.
We attach our platform to it in order to allow the organisation to retain sovereignty over their identities.
Before we close the section on architecture, let’s take a closer look at the Keycloak container view where the platform operator wants to access the Vault instance:
Keycloak’s OIDC endpoint acts as a Vault Authentication method.
Keycloak is configured to use the LDAP external identity store as a source of truth for identities.
If the user us able to provide valid identification credentials for that identity, Keycloak returns valid authorisation claims, and Vault authorises access to the user.
So far, so good but this high-level view of the architecture does not yet reveal the dependencies in terms of platform services and flows.
The component view exposes what underlying platform components are needed:
Platform
Internal developer platform. Contains several planes of services, designed to host customer-specified workloads.
Platform security plane
[Software System]
Contains services related to
access, authoration, security for
platform services
platform personae
platform workloads
Platform resources plane
[Software System]
Provides capacity for workloads in the platform
Platform Data plane
[Software System]
Platform services supporting
data persistence for workloads
Platform workload orchestrator
[Container: Hashicorp Nomad]
The platform workload orchestration system
Platform Identity provider
[Container: OpenLDAP]
The backing service containing actual
human identity and source of truth
Authentication and Authorization
[Container: Keycloak]
Authentication and authorisation layer
for humans and applications.
Secrets engine
[Container: Hashicorp Vault]
Platform service managing security and access for services.
Human authentication via OIDC
Patroni Postgres manager
[Container: Patroni Postgres]
Patroni postgres manager
Platform compute
[Component: Docker, Podman]
CPU and memory resources
available via the orchestrator
So, we have a fairly explicit C4 representation of the architecture.
It shows what we need to bring to the table, and how we integrate with the platform itself.
All we need to do is fill in the boxes with actual technology, and deploy the sucker!
Deployment Scenarios
Let’s define a few hypothetical deployment scenarios.
These can be split grosso-modo between deployments in plane and out of plane.
“In plane” deployments place the services in the Platform Resource Plane, meaning that the services themselves are orchestrated by the platform itself while “out of plane” deployments mean that the services are not orchestrated by the platform, but by some external controller.
This external controller would be something independent, bound to the deploy environment and independently managed, such as a SystemD unit on a virtual machine, or a an independent orchestrator on an external control plane.
Combined deployment in-plane
In this scenario, we deploy the authentication and authorisation services as a single workload into the resource plane.
This means we write a single Nomad job which contains the relevant tasks and services for both the LDAP backing service, as well as the Keycloak service.
As can be seen in the scenario, we are dependent on the existence of a managed PostgreSQL cluster for the deployment of the Keycloak service, which will be expecting a database persistence layer for its data.
Discussion
In this scenario, we have the benefits of:
Single definition of workload: we deploy everything we need with a single nomad.hcl file.
Network namespace sharing: Keycloak can access the LDAP server on the local network since they share a Nomad network
Allocation filesystem sharing: The Nomad allocation system makes available a shared filesystem for the deployment if they need to share files; each task also has access to its own private files and secrets.
However, we also have the potential downsides of:
Resource exhaustion: Since both keycloak and ldap are declared in a single group, they will be placed on a single node. If there are not enough resources, the orchestrator may not be able to place them. This is particularly bad if the node hosting the allocation becomes unhealthy in the cluster.
Monolithic deployment: Somewhat linked to this negative aspect is the fact that the deployment is monolithic - it becomes difficult to independently scale each of the tasks in it. This is not a concern to us initially, but is of great importance when we start to consider production readiness.
As a first step though, this is a good starting point.
It is simple, easy to understand, self-contained, and has specific external dependencies.
Just for the sake of argument, let’s see what a factorised deployment would look like.
Factorised in-plane deployment
The factorised deployment differs from the combined deployment only in that there are two jobs instead of one:
In this scenario, we have almost artificially constructed the reverse of the pro/con set in the previous scenario, but we have introduced a coupling with the service mesh.
We can independently scale the services (Keycloak and LDAP), as well as make them independently resilient.
However, Keycloak will need to have fallback and backoff configuration in case the LDAP service is not discoverable either via DNS or Consul service catalogue lookup.
The failure case here would be that the the LDAP service becomes somehow unavailable, or the Keycloak server cannot connect to it, or perhaps if the template inside the task configuring the LDAP endpoint becomes stale for some reason, authentication would fail.
For now, we will eschew a full-blown risk analysis, contenting ourselves with the inspiration from these two scenarios.
Out-of-plane deployment
The out-of-plane deployment assumes that we have dedicated resources somewhere for these services.
This may be conceived of as a more static deployment model, with dedicated machines in dedicated environments being allocated manually to this workload.
The workloads (LDAP, Keycloak) and perhaps even their direct dependencies (Database backends, DNS resolvers, persistent storage claims, local filesystems, etc) are directly provisioned on these dedicated machines via some form of configuration management10
Final thoughts and summary
We stop short of making a decision of which scenario is “right” here, contenting ourselves with the fact that we have a few scenarios, and can discriminate on them based on what we need to achieve at the time.
A full production-ready deployment must take into account not just the deployment and operation of Day1, but also the ergonomics of change after that, as well as aiming for zero touch on operations.
That would require a deployment in a highly resilient and redundant environment, which we’ve seen mention and glimpses of, but not fully illustrated here.
Supporting those deployments could also be canary deployments where we factorise not just the application servers, but also their persistence layers, migrating them between deploys as we scale up blue, then down green deployments for zero-downtime upgrades.
There is still much analysis to perform, and experience to gain, but we can say at the end of this brief article that:
We have a viable model for deploying platform identity based on the OAuth standard which we can deploy flexibly.
It supports access to the platform services which our operators need to access using the OIDC Connect protocol
If you already have a source of identities, such as an organisational directory, this can be modelled into the architecture and re-used.
Similarly, organisational identities can be factored out from platform identities, but using the exact sample deployment model for the authorisation layer, just with different backing services.
Footnotes and References
We do not yet go into the overall federation of services and access policies for federated users, but we will get there eventually! ↩
Don’t come at me with your weird American spelling. I know everyone calls it AuthN/Z where that zed grinds my nerves, and I know I’m not going to be able to change it. But by Zeus I will spell my own writing properly! ↩
The way I have described it here is far too simplified to be taken seriously, and mainly for my own purposes of creating a short narrative of what components we are deploying. The ecosystem comprising tools and standards to create AAI (Authentication and Authorisation Infrastructure) is far more complex, but it’s not my place to go into it here. ↩
This is an incomplete definition of AAI, solely for the purposes of this article. Good luck finding an authoritative definition of AAI, and if you do, please send it to me. ↩
Recall that when we say “Platform Service”, we are referring to services in platform planes – the orchestrator (Nomad), the secrets engine (Vault), etc. These are not the deployed workloads, often also called “services”, which are configured to use the federated AAI. In the case of EGI services, this AAI service would be Check-In. ↩
As you can see, there are several parts missing. Some are missing by design (we don’t advocate a single IDE, we don’t have a portal, we are not interested in FinOps), while some are still waiting for proper integration (OPA as policy engine, SemGrep and Report Portal as code quality and testing). Stay tuned ↩
I’m not coming right out to say it here, because I wanted to write this as if there were some arbitrary out-of-plane controller here, but it has become unwieldly talking around the fact that this is in all effects a set of Ansible playbooks against a set of VMs. ↩