Abstract
SPOE ( Stream Process Offloading Engine ) allows haproxy to be extended with middlewares. SPOA are agents that talk to haproxy using the Stream Process Offloading Protocol ( SPOP )
Rationale
Why not just a configuration option: Because the oidc-auth SPOA implementation is out of scope of the charm and haproxy should take advantage of relations to be able to work with any SPOA implementations respecting our relation spec.
Specification
Our goal is to support the Canonical Identity Platform authentication flow and use haproxy as an authentication proxy. In this context, every request reaching the haproxy frontend will be passed through the authentication proxy and requires a successful authentication with the IDP to be able to be routed. The spoe-auth relation will be designed as a single-purpose relation due to the amount of OIDC-specific information needed to be communicated through relation data between the provider and requirer.
Following the OIDC spec, the spoe-auth agent charm and the haproxy charm together will serve the function of Relying Party ( RP ).
The spoe-auth agent is expected to handle:
- Validation of requests.
- Building the redirect URL for the open ID connect ( OIDC ) provider.
- Handling OIDC responses
- Perform the authorization code flow to get an access token
- Setting a cookie on the client side to avoid re-auth.
- Validation of the cookie when present on the request
On the other hand, haproxy is expected to manage:
- Routing of OIDC responses to the agent backend
- Sending SPOE messages with the necessary information about the request
- Redirecting to the OIDC provider using the redirect URL provided by the spoe-auth agent
The authentication flow is described using this diagram:
Here’s an overview of the information required between haproxy and the SPOE-auth agent. Haproxy needs to know:
- Through which variables does the SPOE-auth agent communicate important flags such as “Whether the user is authenticated” or “The full URL to issue an IdP redirect”.
- Where will the auth agent handle OIDC responses ( callback path and hostname )
Note: For the first iteration, the spoe-auth relation will be limited to 1 provider application. The logic for handling multiple auth proxies via SPOE will be added if the need arises.
On the spoe-auth agent side, OIDC-specific information to perform the authorization code flow ( client_id and client_secret ) will be provided through the oauth relation, and the redirect URL is constructed by the agent and communicated to haproxy through a variable.
Complete list of spoe-auth relation attributes
It’s not possible to have any sensible defaults for most of the values here since there are no specifications / best practices for them.
Provider ( spoe-auth agent )
| Attribute | Description | Default value |
|---|---|---|
| spop_port | The port on the agent listening for SPOP. | N/A (Required) |
| oidc_callback_port | The port on the agent handling OIDC callbacks | N/A (Required) |
| event | The event that triggers SPOE messages | N/A (Required) |
| var_authenticated | Name of the variable set by the SPOE agent to indicate that a request is authenticated | N/A (Required) |
| var_redirect_url | Name of the variable set by the SPOE agent that contains the full redirect URL to IDP ( including callback, state, etc… ) | N/A (Required) |
| cookie_name | Name of the cookie that the SPOE agent uses to manage authentication | N/A (Required) |
| oidc_callback_path | Path that the agent uses to handle callback. Haproxy will always route this path back to the SPOE agent’s callback port | /oauth2/callback |
| oidc_callback_hostname | The hostname that haproxy will route to to handle OIDC callbacks. The spoe-auth agent is expected to communicate the url https:///<oidc_callback_path> as authorized redirect_uri on the IDP | N/A (Required) |
Note: Future iterations of the interface can provide the possibility of configuring the SPOP (Stream Process Offloading Protocol ) connection
Example spoe relation data
Provider ( spoe-auth agent )
application-data: {
"type": "oidc",
"spop_port": 12345,
"event": "on-http-request",
"var_authenticated": "sess.auth.is_authenticated",
"var_redirect_url": "sess.auth.redirect_url",
"cookie_name": "sessioncookie",
"oidc_callback_port": 5000,
"oidc_callback_path": "/oauth2/callback",
"oidc_callback_hostname": "auth.haproxy.internal",
}
unit-data: {
unit/0: {address: 10.0.0.1}
}
Example haproxy configuration
For the provider, haproxy will create a configuration file in /etc/haproxy/spoe_auth.conf. Below is the template for the configuration files. Parameters provided during template rendering are marked in red.
[spoe-auth]
spoe-agent spoe-auth-agents
messages oidc
option var-prefix auth
use-backend spop_auth
spoe-message oidc
args arg_ssl=ssl_fc arg_oidc_callback_host=str({{ oidc_callback_hostname }}) arg_host=req.hdr(Host) arg_pathq=pathq arg_cookie=req.cook({{ cookie_name }})
event {{ event }}
The spoe-auth provider charm should implement the oauth relation, as well as decide on a hostname so that haproxy can route oidc callbacks to https:///<oidc_callback_path> to the spoe_auth agent.
In the main haproxy configuration file, haproxy will handle setting up the redirection backend, SPOP backend, OIDC callback backend, as well as calling the agent.
frontend haproxy
...
# Immediately forward OIDC responses to the callback URL
acl oidc_callback_path path_beg -i {{ oidc_callback_path }}
acl oidc_callback_host req.hdr(Host) -m str {{ oidc_callback_hostname }}
use_backend spoe_oidc_callback if oidc_callback_path oidc_callback_host
# Call the SPOE agent
filter spoe engine spoe-auth config /etc/haproxy/spoe_auth.conf
# Fetch the authenticated flag set with the SPOP response, redirect to IDP if not authenticated
acl spoe_auth_authenticated var({{ var_authenticated }}) -m bool
use_backend backend_redirect if ! authenticated
...
backend backend_redirect
mode http
balance roundrobin
http-request redirect location %[var({{ var_redirect_url }})]
backend spoe_oidc_callback
{% for agent in spoe_agents %}
server {{ agent.name }} {{ agent.address }}:{{ agent.oidc_callback_port }} check
{% endfor %}
backend spop_auth
mode tcp
balance roundrobin
option spop-check
{% for agent in spoe_agents %}
server {{ agent.name }} {{ agent.address }}:{{ agent.oidc_callback_port }} check
{% endfor %}
Other important considerations
spoe_auth agent requires the following arguments:
arg_ssl=ssl_fc
arg_host=req.hdr(Host)
arg_pathq=pathq
arg_cookie=req.cook(authsession)
arg_client_id(optional)=var(req.oidc_client_id)
arg_client_secret(optional)=var(req.oidc_client_secret)
arg_redirect_url (optional)=var(req.oidc_redirect_url)
arg_token_claims (optional)=var(req.oidc_token_claims)
Initially we want to have a general SPOE relation that provides the flexibility for haproxy to integrate with any external SPOE agent and that the relation can evolve over time. However, as haproxy processes SPOE agents interaction one at a time, ordering is an important factor. For example, a WAF or DDOS protection SPOA might need to be processed before an auth-proxy SPOA. In this regard, it makes more sense to concretely identify each use case and expose each of them as separate relations.
With that in mind, this spec will cover high-level design for a spoe-auth relation. This has 3 main advantages:
- Expectation is implied through the relation name ( args, messages, which args are required per message, …) The scope for a generic relation is too large to be effectively modelled.
- We can lay out a clear ruleset / specification on what haproxy expect for an oidc-auth SPOA and what is the expected haproxy+SPOA configuration
- Templating is relatively easy, reduce the load on haproxy