[specification] ISD257 - Haproxy auth proxy using spoe-auth relation

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:

  1. Validation of requests.
  2. Building the redirect URL for the open ID connect ( OIDC ) provider.
  3. Handling OIDC responses
  4. Perform the authorization code flow to get an access token
  5. Setting a cookie on the client side to avoid re-auth.
  6. Validation of the cookie when present on the request

On the other hand, haproxy is expected to manage:

  1. Routing of OIDC responses to the agent backend
  2. Sending SPOE messages with the necessary information about the request
  3. 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:

  1. 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”.
  2. 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
1 Like