goauthentik / authentik

The authentication glue you need.
https://goauthentik.io
Other
12.56k stars 843 forks source link

Allow role based access control + Hostname pattern matching for domain level forward auth #2807

Open strazto opened 2 years ago

strazto commented 2 years ago

It's pretty much in the title - https://www.authelia.com/docs/configuration/access-control.html authelia provides a good reference

I tried to roll my own, but i couldn't figure out how to access the original host - Also i think this use case is common enough to warrant a feature

Here was my attempt at an expression policy that achieved this

dest_host_key = 'X-Forwarded-Host-Original'
protected_subdomains = ['movies']

headers = request.http_request.headers

dest_host = headers[dest_host_key] if dest_host_key in headers else ""

ak_logger.info(f"Destination Host: {dest_host}")
if not dest_host:
    ak_message(f"No value found for {dest_host_key}")
    return False

ak_message(f"Let's try to auth to {dest_host}")
ak_message(f"Headers: {headers}")
host_components = dest_host.split('.')

# Determine if subdomain should be protected

require_admin = False

for host_component in host_components:
    if regex_match(host_component, r"^([:alpha:]+-)?admin$"):
        require_admin = True

# Or if last domain component is a protected app
for subdomain in protected_subdomains:
    if subdomain == host_components[0]:
        require_admin = True

if not require_admin:
    ak_message(f"{dest_host} does not require admin.")
    return True

is_admin = ak_is_group_member(request.user, name = "authentik Admins")

if not is_admin:
    ak_message(f"{request.user} is not an admin but {dest_host} requires admin!")
    return False

ak_message(f"{request.user} is an admin and {dest_host} requires admin, granting acess!")
return True

apologies for the above devolving into debug spam

For now, I'm just going to make a separate google oauth app for admin forward auth

strazto commented 2 years ago

Update on this - I just realized I can set up an OIDC provider in authentik, and create an application that applies a group membership policy, then configure https://github.com/thomseddon/traefik-forward-auth to use the authentik provider.

This is a relatively lightweight way to achieve what I need, which is really just separation of a few groups of users.

BeryJu commented 2 years ago

The way domain-level authentication works, this can't be implemented currently. The outpost is go and the core is python, and authorization is only done during the authentication of the user. The outpost could use the API to run policies and further check access, however this would be very slow.

strazto commented 2 years ago

@BeryJu thanks for the reply - What you're saying is that the outpost processes the requests, and the core checks access, and when expression policies are run, it's by core, and core doesn't really have access to the same context as the outpost?

BeryJu commented 2 years ago

The outpost checks if the request has a valid auth cookie, if not redirects the user to the core for a full OAuth flow, where the authentication and authorization happens, which the outpost then gets and writes said cookie.

BeryJu commented 2 years ago

in a world where the entire backend is Go, this would still happen similarly, except the outpost would talk directly to the core via GRPC/etc to do additional checks, and this would also allow property mappings in the proxy provider to change instantly vs. currently requiring users to re-login

strazto commented 2 years ago

I don't know whether there are plans to migrate the backend eventually to make this viable, but I've actually configured the traefik-forward-auth instance I was previously using for super-user apps to use authentik as an OIDC provider, and the authentik-side app to check admin access, and I'm very happy with the result, so I'm not too bothered.

strazto commented 2 years ago

This is roughly what my setup looks like.

You need to configure an OIDC provider application in authentik, and apply some kind of admin membership policy to that ( https://authentik.${root_domain}/application/o/admin/ )

For regular users, we make the standard proxy outpost, with whatever policies should apply to that.

This applies at the domain level.

We configure traefik-forward-auth to use https://authentik.${root_domain}/application/o/admin/ as its id issuer.

To quickly configure a service to use one policy or the other, we just set it's middleware to either authentik@docker or traefik-forward-auth (if its admin)


services:
   whoami-user:
      traefik.http.routers.whoami-admin.rule: Host(`whoami-user.${root_domain}`)
      # service
      traefik.http.services.whoami-user.loadbalancer.server.port: "8080"
      traefik.http.routers.whoami-user.middlewares: authentik@docker
   whoami-admin:
      traefik.http.routers.whoami-admin.rule: Host(`whoami-admin.${root_domain}`)
      # service
      traefik.http.services.whoami-admin.loadbalancer.server.port: "8080"
      traefik.http.routers.whoami-admin.middlewares: traefik-forward-auth
  # OIDC forward authentication for traefik
  traefik-forward-auth:
    image: thomseddon/traefik-forward-auth:latest
    <<: *restart
    <<: *logging
    mem_limit: 100m
    environment:
      # PROVIDERS_GOOGLE_CLIENT_ID: ${OAUTH2_PROXY_CLIENT_ID}
      # PROVIDERS_GOOGLE_CLIENT_SECRET: ${OAUTH2_PROXY_CLIENT_SECRET}
      SECRET: ${OAUTH2_PROXY_COOKIE_SECRET}
      DEFAULT_PROVIDER: oidc
      PROVIDERS_OIDC_ISSUER_URL: https://authentik.${root_domain}/application/o/admin/
      PROVIDERS_OIDC_CLIENT_ID: ${OAUTH2_PROXY_CLIENT_ID}
      PROVIDERS_OIDC_CLIENT_SECRET: ${OAUTH2_PROXY_CLIENT_SECRET}

      AUTH_HOST: "oauth.${root_domain}"
      COOKIE_DOMAIN: "${root_domain}"
      COOKIE_NAME: _admin_forward_auth
      CSRF_COOKIE_NAME: _admin_forward_auth_csrf
      # This is in seconds
      LIFETIME: "86400"
    networks:
      - traefik
    labels:
      <<: *traefik
      # HTTP entrypoint
      traefik.http.routers.traefik-forward-auth.rule: Host(`oauth.${root_domain}`)
      # service
      traefik.http.services.traefik-forward-auth.loadbalancer.server.port: "4181"
      # Forward auth
      traefik.http.middlewares.traefik-forward-auth.forwardauth.address: http://traefik-forward-auth:4181
      traefik.http.routers.traefik-forward-auth.middlewares: traefik-forward-auth
      traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders: X-Forwarded-User

  authentik:
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.4.1}
    restart: unless-stopped
    networks:
      # TODO: Change to own networks (auth proxy + internal auth)
       - traefik
    command: server
    environment:
      <<: *conf
      PGID: ${docker_group_id}
      AUTHENTIK_SECRET_KEY: "${AUTHENTIK_SECRET_KEY:?Need a secret key lol}"
      AUTHENTIK_REDIS__HOST: authentik-redis
      AUTHENTIK_POSTGRESQL__HOST: authentik-pg
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_POSTGRESQL__PASSWORD}

      AUTHENTIK_EMAIL__HOST: ${AUTHENTIK_EMAIL__HOST}
      AUTHENTIK_EMAIL__PORT: ${AUTHENTIK_EMAIL__PORT}
      AUTHENTIK_EMAIL__USERNAME: ${AUTHENTIK_EMAIL__USERNAME}
      AUTHENTIK_EMAIL__PASSWORD: ${AUTHENTIK_EMAIL__PASSWORD}
      AUTHENTIK_EMAIL__USE_TLS: "true"
      AUTHENTIK_EMAIL__USE_SSL: "false"
      AUTHENTIK_EMAIL__TIMEOUT: 10
      AUTHENTIK_EMAIL__FROM: ${AUTHENTIK_EMAIL__FROM}
      # AUTHENTIK_ERROR_REPORTING__ENABLED: "true"
      # WORKERS: 2
    volumes:
      - ${docker_data_folder}/authentik/media:/media
      - ${docker_data_folder}/authentik/templates:/templates
    #// ports:
     #// - "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
     #// - "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
    labels:
      <<: *traefik
      traefik.http.routers.authentik.rule: Host(`authentik.${root_domain}`) 
      # && PathPrefix(`/outpost.goauthentik.io/`)
      traefik.http.services.authentik.loadbalancer.server.port: "9000"      
      # `authentik-proxy` refers to the service name in the compose file.

      traefik.http.middlewares.authentik.forwardauth.address: http://authentik: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