mother-of-all-self-hosting / mash-playbook

🐋 Ansible playbook which helps you host various FOSS services as Docker containers on your own server
GNU Affero General Public License v3.0
466 stars 60 forks source link

Using Authentic to secure services (especially those without authentication) #50

Open Mijago opened 1 year ago

Mijago commented 1 year ago

Hello,

Now that we have Authentic (and keycloak), I want to propose that we add flags that allow us to secure any service with Authentic. We may want to add more services in the future, also services that do not have their own login methods. We could easily secure them behind Authentic now.

If I understood the documentation correctly, then the proxy provider sounds like it is made for exactly this scenario: vivaldi_sreveIxdWg This supports traefik's forwardAuth labels, so in theory, a few simple flags could already solve this.

Scope of this proposal:

Mijago commented 1 year ago

Relevant documentation links:

Maybe Authentic can do this already right from the interface, then I just missed it - nonetheless, the discussion in this issue can probably improve the setup documentation for Authentic.

spantaleev commented 1 year ago

Sound slike you're proposing some tighter coupling between the roles. In matrix-docker-ansible-deploy we did stuff like this - you enable some component and via group vars it becomes automatically wired into other components.

It's very convenient, but.. it's also very magical and makes it hard to work on the playbook later on.

With multiple competing implementations of the same thing, the problem gets worse.


For this MASH playbook, we're intentionally trying to keep roles more independent and to not auto-wire them automagically via group vars. As the number of roles gets larger, it would lead to implementation complexity and unexpected bugs to have things wired automatically.

Our goal with MASH is to provide good high quality and customizable roles, which users can plug together as they see fit. No assumptions. No magic.


That said, roles may be made to expose some way to customize the middleware labels and/or to inject a forwardauth middleware, thus making it easy to plug Authentik (and Authelia or other such implementations) in the future.

moan0s commented 1 year ago

I think it would be great to have an example and generic setup Forward Auth of that in the documentation!

Implement a proof-of-concept for one service, then migrate it to all other services with http endpoint/s.

I think picking a role like prometheus which traditionally uses basic auth could be a good start. Are you going to do that?

This probably must be added to every service, but after it has been set up once it can probaby be copy-pasted to every other container.

I thnik I'd be in favor of this. We should turn it off by default and not do some magic there but it would be nice to have it set up as easy as e.g. coupling redis to nextcloud (so one/a few entries in vars.yml set the middleware up as @spantaleev described)

We already reuse large parts of the traefik middleware, so adding this middleware to every role would be similar to what we already do.

Mijago commented 1 year ago

Hi, sadly I do not have as much time to play around as I anticipated. I hope I can get back to this next week and try a few things.

Mijago commented 1 year ago

A small documentation of the things I learned so far:

Authentik can be used to add authentication to services that do not have their own authentication, using the "forward-auth" from our traefik.

Scenarios

We can either have

Modification of the to-be-authenticated service

In all cases, a service itself will just be modified with one simple traefik label to add the authentik@docker rule:

    whoami:
        image: containous/whoami
        labels:
            traefik.enable: true
            traefik.http.routers.whoami.rule: Host(`auth.MYDOMAIN.net`)
            traefik.http.routers.whoami.middlewares: authentik@docker
        restart: unless-stopped

Authentik Proxy

No matter which scenario we have, we need (at least) one ghcr.io/goauthentik/proxy container. This container forwards Host(auth.MYDOMAIN.net) && PathPrefix(/outpost.goauthentik.io/) to the authentik middleware. In my case I have Authentik at auth.MYDOMAIN.net and only have one proxy for all services:

Example from the documentation:

    authentik-proxy:
        image: ghcr.io/goauthentik/proxy
        ports:
            - 9000:9000
            - 9443:9443
        environment:
            AUTHENTIK_HOST: https://auth.MYDOMAIN.net
            AUTHENTIK_INSECURE: "false"
            AUTHENTIK_TOKEN: token-generated-by-authentik
            # Starting with 2021.9, you can optionally set this too
            # when authentik_host for internal communication doesn't match the public URL
            # AUTHENTIK_HOST_BROWSER: https://external-domain.tld
        labels:
            traefik.enable: true
            traefik.port: 9000
            traefik.http.routers.authentik.rule: Host(`auth.MYDOMAIN.net`) && PathPrefix(`/outpost.goauthentik.io/`)
            # `authentik-proxy` refers to the service name in the compose file.
            traefik.http.middlewares.authentik.forwardauth.address: http://authentik-proxy:9000/outpost.goauthentik.io/auth/traefik
            traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true
            traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version
        restart: unless-stopped

Authentik Configuration

Proxy Auth (single application)

General steps are like this:

  1. Create a Provider. Select Proxy Provider, fill Name and External host and pick a flow.
  2. Create an Application. Enter a name and select your previously created provider.
  3. Create a new Outpost (only once). I called it Base Domain, Type Proxy. Select the previously created Application.

That's it.

Adding headers

I'll show an example to fill X-authentik-email with the email of the authenticated user. More info here.

  1. Create a Property Mapping. Like this:
    return {
      "ak_proxy": {
          "user_attributes": {
              "additionalHeaders": {
                  "X-authentik-email": request.user.email
              }
          }
      }
    }

tl;dr

We do not need to add a lot:

nielscil commented 1 year ago

The default installation contains a embedded proxy, so the additional container is not needed.

What I did to implement the forward auth and proxy is the following. I'm using it for external services so not with mash services and traefik labels yet.

Forward auth

Authentik acts as forwardAuth middleware. Traefik should proxy all services with PathPrefix(`/outpost.goauthentik.io/`) . The service itself needs the middleware.

devture_traefik_provider_configuration_extension_yaml: |
  http:
    middlewares:
      authentik:
        forwardAuth:
          address: http://{{ authentik_server_identifier }}:{{ authentik_container_http_port  }}/outpost.goauthentik.io/auth/traefik
          trustForwardHeader: true
          authResponseHeaders:
            - X-authentik-username
            - X-authentik-groups
            - X-authentik-email
            - X-authentik-name
            - X-authentik-uid
            - X-authentik-jwt
            - X-authentik-meta-jwks
            - X-authentik-meta-outpost
            - X-authentik-meta-provider
            - X-authentik-meta-app
            - X-authentik-meta-version
    routers:
      test-one:
        rule: "Host(`test2.example.com`)"
        middlewares:
          - authentik
        service: test-one
    services:
      test-one:
        loadBalancer:
          servers:
            - url: http://test2.example.internal

# Custom variable for keeping list and mapping to host rule
authentik_forward_auth_hosts:
  - "test2.example.com"

authentik_forward_auth_host_rule: "Host({{ authentik_forward_auth_hosts | map('regex_replace','^(.+)$','`\\1`') | list  | join(', ') }}) && PathPrefix(`/outpost.goauthentik.io/`)"

Proxy

Authentik acts as reverse proxy. Traefik should send all traffic for services to authentik.

# Custom variable for keeping list and mapping to host rule
authentik_proxy_hosts:
  - "test1.example.com"

authentik_proxy_host_rule: "Host({{ authentik_proxy_hosts | map('regex_replace','^(.+)$','`\\1`') | list  | join(', ') }})"

Override Authentik rule

The main Authentik Traefik rule needs to be altered with the rules above.

authentik_container_labels_traefik_rule: "Host(`{{ authentik_container_labels_traefik_hostname }}`) || ({{ authentik_proxy_host_rule }}) || ({{ authentik_forward_auth_host_rule }})"

I think we should introduce in the authentik role some variables to define the proxy and forward hosts and fill them in the authentik_container_labels_traefik_rule like above. Besides that we should make it possible to define additional middlewares for the services.