mirror of
https://github.com/funkypenguin/geek-cookbook/
synced 2025-12-13 01:36:23 +00:00
Update Traefik Forward Auth for v2 (#169)
This commit is contained in:
@@ -4,107 +4,50 @@ Now that we have Traefik deployed, automatically exposing SSL access to our Dock
|
||||
|
||||
..Wait, why not? Well, Traefik doesn't provide any form of authentication, it simply secures the **transmission** of the service between Docker Swarm and the end user. If you were to deploy a service with no native security (*[Radarr](/recipes/autopirate/radarr/) or [Sonarr](/recipes/autopirate/sonarr/) come to mind*), then anybody would be able to use it! Even services which _may_ have a layer of authentication **might** not be safe to expose publically - often open source projects may be maintained by enthusiasts who happily add extra features, but just pay lip service to security, on the basis that "*it's the user's problem to secure it in their own network*".
|
||||
|
||||
To give us confidence that **we** can access our services, but BadGuys(tm) cannot, we'll deploy a layer of authentication **in front** of Traefik, using [Forward Authentication](https://docs.traefik.io/configuration/entrypoints/#forward-authentication). You can use your own [KeyCloak](/recipes/keycloak/) instance for authentication, but to lower the barrier to entry, this recipe will assume you're authenticating against your own Google account.
|
||||
Some of the platforms we use on our swarm may have strong, proven security to prevent abuse. Techniques such as rate-limiting (*to defeat brute force attacks*) or even support 2-factor authentication (*tiny-tiny-rss or Wallabag support this)*.
|
||||
|
||||
## Ingredients
|
||||
Other platforms may provide **no authentication** (Traefik's web UI for example), or minimal, un-proven UI authentication which may have been added as an afterthought.
|
||||
|
||||
!!! summary "Ingredients"
|
||||
Existing:
|
||||
Still other platforms may hold such sensitive data (*i.e., NextCloud*), that we'll feel more secure by putting an additional authentication layer in front of them.
|
||||
|
||||
* [X] [Docker swarm cluster](/ha-docker-swarm/design/) with [persistent shared storage](/ha-docker-swarm/shared-storage-ceph)
|
||||
* [X] [Traefik](/ha-docker-swarm/traefik/) configured per design
|
||||
This is the role of Traefik Forward Auth.
|
||||
|
||||
New:
|
||||
## How does it work?
|
||||
|
||||
* [ ] Client ID and secret from an OpenID-Connect provider (Google, [KeyCloak](/recipes/keycloak/), Microsoft, etc..)
|
||||
**Normally**, Traefik proxies web requests directly to individual web apps running in containers. The user talks directly to the webapp, and the webapp is responsible for ensuring appropriate authentication.
|
||||
|
||||
## Preparation
|
||||
When employing Traefik Forward Auth as "[middleware](https://doc.traefik.io/traefik/middlewares/forwardauth/)", the forward-auth process sits in the middle of this transaction - traefik receives the incoming request, "checks in" with the auth server to determine whether or not further authentication is required. If the user is authenticated, the auth server returns a 200 response code, and Traefik is authorized to forward the request to the backend. If not, traefik passes the auth server response back to the user - this process will usually direct the user to an authentication provider (_GitHub, Google, etc_), so that they can perform a login.
|
||||
|
||||
### Obtain OAuth credentials
|
||||
Illustrated below:
|
||||

|
||||
|
||||
!!! note
|
||||
This recipe will demonstrate using Google OAuth for traefik forward authentication, but it's also possible to use a self-hosted KeyCloak instance - see the [KeyCloak OIDC Provider](/recipes/keycloak/setup-oidc-provider/) recipe for more details!
|
||||
The advantage under this design is additional security. If I'm deploying a web app which I expect only an authenticated user to require access to (*unlike something intended to be accessed publically, like [Linx](/recipes/linx/)*), I'll pass the request through Traefik Forward Auth. The overhead is negligible, and the additional layer of security is well-worth it.
|
||||
|
||||
Log into https://console.developers.google.com/, create a new project then search for and select "Credentials" in the search bar.
|
||||
## What is AuthHost mode
|
||||
|
||||
Fill out the "OAuth Consent Screen" tab, and then click, "**Create Credentials**" > "**OAuth client ID**". Select "**Web Application**", fill in the name of your app, skip "**Authorized JavaScript origins**" and fill "**Authorized redirect URIs**" with either all the domains you will allow authentication from, appended with the url-path (*e.g. https://radarr.example.com/_oauth, https://radarr.example.com/_oauth, etc*), or if you don't like frustration, use a "auth host" URL instead, like "*https://auth.example.com/_oauth*" (*see below for details*)
|
||||
Under normal OIDC auth, you have to tell your auth provider which URLs it may redirect an authenticated user back to, post-authentication. This is a security feture of the OIDC spec, preventing a malicious landing page from capturing your session and using it to impersonate you. When you're securing many URLs though, explicitly listing them can be a PITA.
|
||||
|
||||
!!! tip
|
||||
Store your client ID and secret safely - you'll need them for the next step.
|
||||
[@thomaseddon's traefik-forward-auth](https://github.com/thomseddon/traefik-forward-auth) includes an ingenious mechanism to simulate an "_auth host_" in your OIDC authentication, so that you can protect an unlimited amount of DNS names (_with a common domain suffix_), without having to manually maintain a list.
|
||||
|
||||
#### How does it work?
|
||||
|
||||
### Prepare environment
|
||||
Say you're protecting **radarr.example.com**. When you first browse to **https://radarr.example.com**, Traefik forwards your session to traefik-forward-auth, to be authenticated. Traefik-forward-auth redirects you to your OIDC provider's login (_KeyCloak, in this case_), but instructs the OIDC provider to redirect a successfully authenticated session **back** to **https://auth.example.com/_oauth**, rather than to **https://radarr.example.com/_oauth**.
|
||||
|
||||
Create `/var/data/config/traefik-forward-auth/traefik-forward-auth.env` as follows:
|
||||
When you successfully authenticate against the OIDC provider, you are redirected to the "_redirect_uri_" of https://auth.example.com. Again, your request hits Traefik, which forwards the session to traefik-forward-auth, which **knows** that you've just been authenticated (_cookies have a role to play here_). Traefik-forward-auth also knows the URL of your **original** request (_thanks to the X-Forwarded-Whatever header_). Traefik-forward-auth redirects you to your original destination, and everybody is happy.
|
||||
|
||||
```
|
||||
GOOGLE_CLIENT_ID=<your client id>
|
||||
GOOGLE_CLIENT_SECRET=<your client secret>
|
||||
OIDC_ISSUER=https://accounts.google.com
|
||||
SECRET=<a random string, make it up>
|
||||
# uncomment this to use a single auth host instead of individual redirect_uris (recommended but advanced)
|
||||
#AUTH_HOST=auth.example.com
|
||||
COOKIE_DOMAINS=example.com
|
||||
```
|
||||
This clever workaround only works under 2 conditions:
|
||||
|
||||
### Prepare the docker service config
|
||||
1. Your "auth host" has the same domain name as the hosts you're protecting (_i.e., auth.example.com protecting radarr.example.com_)
|
||||
2. You explictly tell traefik-forward-auth to use a cookie authenticating your **whole** domain (_i.e. example.com_)
|
||||
|
||||
Create `/var/data/config/traefik-forward-auth/traefik-forward-auth.yml` as follows:
|
||||
## Authentication Providers
|
||||
|
||||
```
|
||||
traefik-forward-auth:
|
||||
image: thomseddon/traefik-forward-auth:2.1.0
|
||||
env_file: /var/data/config/traefik-forward-auth/traefik-forward-auth.env
|
||||
networks:
|
||||
- traefik_public
|
||||
# Uncomment these lines if you're using auth host mode
|
||||
#deploy:
|
||||
# labels:
|
||||
# - traefik.port=4181
|
||||
# - traefik.frontend.rule=Host:auth.example.com
|
||||
# - traefik.frontend.auth.forward.address=http://traefik-forward-auth:4181
|
||||
# - traefik.frontend.auth.forward.trustForwardHeader=true
|
||||
```
|
||||
Traefik Forward Auth needs to authenticate an incoming user against a provider. A provider can be something as simple as a self-hosted [dex][tfa-dex] instance with a single static username/password, or as complex as a [KeyCloak][keycloak] instance backed by [OpenLDAP][openldap]. Here are some options, in increasing order of complexity...
|
||||
|
||||
If you're not confident that forward authentication is working, add a simple "whoami" test container to the above .yml, to help debug traefik forward auth, before attempting to add it to a more complex container.
|
||||
* [Authenticate against a self-hosted Dex instance with static usernames and passwords][tfa-dex-static]
|
||||
* [Authenticate against a whitelist of Google accounts][tfa-google]
|
||||
* [Authenticate against a self-hosted KeyCloak instance][tfa-keycloak]
|
||||
|
||||
```
|
||||
# This simply validates that traefik forward authentication is working
|
||||
whoami:
|
||||
image: containous/whoami
|
||||
networks:
|
||||
- traefik_public
|
||||
deploy:
|
||||
labels:
|
||||
- traefik.frontend.rule=Host:whoami.example.com
|
||||
- traefik.port=80
|
||||
- traefik.frontend.auth.forward.address=http://traefik-forward-auth:4181
|
||||
- traefik.frontend.auth.forward.authResponseHeaders=X-Forwarded-User
|
||||
- traefik.frontend.auth.forward.trustForwardHeader=true
|
||||
```
|
||||
--8<-- "recipe-footer.md"
|
||||
|
||||
--8<-- "premix-cta.md"
|
||||
|
||||
## Serving
|
||||
|
||||
### Launch
|
||||
|
||||
Redeploy traefik with ```docker stack deploy traefik-forward-auth -c /var/data/traefik-forward-auth/traefik-forward-auth.yml```, to launch the traefik-forward-auth stack.
|
||||
|
||||
### Test
|
||||
|
||||
Browse to https://whoami.example.com (*obviously, customized for your domain and having created a DNS record*), and all going according to plan, you should be redirected to a Google login. Once successfully logged in, you'll be directed to the basic whoami page.
|
||||
|
||||
## Summary
|
||||
|
||||
What have we achieved? By adding an additional three simple labels to any service, we can secure any service behind our choice of OAuth provider, with minimal processing / handling overhead.
|
||||
|
||||
!!! summary "Summary"
|
||||
Created:
|
||||
|
||||
* [X] Traefik-forward-auth configured to authenticate against an OIDC provider
|
||||
|
||||
[^1]: Traefik forward auth replaces the use of [oauth_proxy containers](/reference/oauth_proxy/) found in some of the existing recipes
|
||||
[^2]: I reviewed several implementations of forward authenticators for Traefik, but found most to be rather heavy-handed, or specific to a single auth provider. @thomaseddon's go-based docker image is 7MB in size, and can be extended to work with any OIDC provider.
|
||||
|
||||
--8<-- "recipe-footer.md"
|
||||
[^1]: Authhost mode is specifically handy for Google authentication, since Google doesn't permit wildcard redirect_uris, like [KeyCloak][keycloak] does.
|
||||
200
manuscript/ha-docker-swarm/traefik-forward-auth/dex-static.md
Normal file
200
manuscript/ha-docker-swarm/traefik-forward-auth/dex-static.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Using Traefik Forward Auth with Dex (Static)
|
||||
|
||||
[Traefik Forward Auth](/ha-docker-swarm/traefik-forward-auth/) is incredibly useful to secure services with an additional layer of authentication, provided by an OIDC-compatible provider. The simplest possible provider is a self-hosted instance of [CoreOS's Dex](https://github.com/dexidp/dex), configured with a static username and password. This recipe will "get you started" with Traefik Forward Auth, providing a basic authentication layer. In time, you might want to migrate to a "public" provider, like [Google][tfa-google], or GitHub, or to a [KeyCloak][keycloak] installation.
|
||||
|
||||
--8<-- "recipe-tfa-ingredients.md"
|
||||
|
||||
## Preparation
|
||||
|
||||
### Setup dex config
|
||||
|
||||
Create `/var/data/config/dex/config.yml` something like the following (*this is a bare-bones, [minimal example](https://github.com/dexidp/dex/blob/master/config.dev.yaml)*). At the very least, you want to replace all occurances of `example.com` with your own domain name. (*If you change nothing else, your ID is `foo`, your secret is `bar`, your username is `admin@yourdomain`, and your password is `password`*):
|
||||
|
||||
```yaml
|
||||
# The base path of dex and the external name of the OpenID Connect service.
|
||||
#
|
||||
# This is the canonical URL that all clients MUST use to refer to dex. If a
|
||||
# path is provided, dex's HTTP service will listen at a non-root URL.
|
||||
issuer: https://dex.example.com
|
||||
|
||||
storage:
|
||||
type: sqlite3
|
||||
config:
|
||||
file: var/sqlite/dex.db
|
||||
|
||||
web:
|
||||
http: 0.0.0.0:5556
|
||||
|
||||
oauth2:
|
||||
skipApprovalScreen: true
|
||||
|
||||
staticClients:
|
||||
- id: foo
|
||||
redirectURIs:
|
||||
- 'https://auth.example.com/_oauth'
|
||||
name: 'example.com'
|
||||
secret: bar
|
||||
|
||||
staticPasswords:
|
||||
- email: "admin@example.com"
|
||||
# bcrypt hash of the string "password"
|
||||
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||
username: "admin"
|
||||
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
|
||||
```
|
||||
|
||||
### Prepare Traefik Forward Auth environment
|
||||
|
||||
Create `/var/data/config/traefik-forward-auth/traefik-forward-auth.env` as follows:
|
||||
|
||||
```
|
||||
DEFAULT_PROVIDER: oidc
|
||||
PROVIDERS_OIDC_CLIENT_ID: foo # This is the staticClients.id value in config.yml above
|
||||
PROVIDERS_OIDC_CLIENT_SECRET: bar # This is the staticClients.secret value in config.yml above
|
||||
PROVIDERS_OIDC_ISSUER_URL: https://dex.example.com # This is the issuer value in config.yml above, and it has to be reachable via a browser
|
||||
SECRET: imtoosexyformyshorts # Make this up. It's not configured anywhere else
|
||||
AUTH_HOST: auth.example.com # This should match the value of the traefik hosts labels in Traefik Forward Auth
|
||||
COOKIE_DOMAIN: example.com # This should match your base domain
|
||||
```
|
||||
|
||||
### Setup Docker Stack for Dex
|
||||
|
||||
Create a docker swarm config file in docker-compose syntax (v3), something like this:
|
||||
|
||||
--8<-- "premix-cta.md"
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
dex:
|
||||
image: dexidp/dex
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/data/config/dex/config.yml:/config.yml:ro
|
||||
networks:
|
||||
- traefik_public
|
||||
command: ['serve','/config.yml']
|
||||
deploy:
|
||||
labels:
|
||||
# traefik
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik_public
|
||||
|
||||
# traefikv1
|
||||
- traefik.frontend.rule=Host:dex.example.com
|
||||
- traefik.port=5556
|
||||
- traefik.docker.network=traefik_public
|
||||
|
||||
# and for traefikv2:
|
||||
- "traefik.http.routers.dex.rule=Host(`dex.example.com`)"
|
||||
- "traefik.http.routers.dex.entrypoints=https"
|
||||
- "traefik.http.services.dex.loadbalancer.server.port=5556"
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
```
|
||||
|
||||
### Setup Docker Stack for Traefik Forward Auth
|
||||
|
||||
Now create a docker swarm config file in docker-compose syntax (v3), something like this:
|
||||
|
||||
```yaml
|
||||
version: "3.2"
|
||||
|
||||
services:
|
||||
|
||||
traefik-forward-auth:
|
||||
image: thomseddon/traefik-forward-auth:2.1.0
|
||||
env_file: /var/data/config/traefik-forward-auth/traefik-forward-auth.env
|
||||
volumes:
|
||||
- /var/data/config/traefik-forward-auth/config.ini:/config.ini:ro
|
||||
networks:
|
||||
- traefik_public
|
||||
deploy:
|
||||
labels:
|
||||
# traefikv1
|
||||
- "traefik.port=4181"
|
||||
- "traefik.frontend.rule=Host:auth.example.com"
|
||||
- "traefik.frontend.auth.forward.address=http://traefik-forward-auth:4181"
|
||||
- "traefik.frontend.auth.forward.trustForwardHeader=true"
|
||||
|
||||
# traefikv2
|
||||
- "traefik.docker.network=traefik_public"
|
||||
- "traefik.http.routers.auth.rule=Host(`auth.example.com`)"
|
||||
- "traefik.http.routers.auth.entrypoints=https"
|
||||
- "traefik.http.routers.auth.tls=true"
|
||||
- "traefik.http.routers.auth.tls.domains[0].main=example.com"
|
||||
- "traefik.http.routers.auth.tls.domains[0].sans=*.example.com"
|
||||
- "traefik.http.routers.auth.tls.certresolver=main"
|
||||
- "traefik.http.routers.auth.service=auth@docker"
|
||||
- "traefik.http.services.auth.loadbalancer.server.port=4181"
|
||||
- "traefik.http.middlewares.forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
|
||||
- "traefik.http.middlewares.forward-auth.forwardauth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
|
||||
- "traefik.http.routers.auth.middlewares=forward-auth"
|
||||
|
||||
# This simply validates that traefik forward authentication is working
|
||||
whoami:
|
||||
image: containous/whoami
|
||||
networks:
|
||||
- traefik_public
|
||||
deploy:
|
||||
labels:
|
||||
# traefik
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik_public"
|
||||
|
||||
# traefikv1
|
||||
- "traefik.frontend.rule=Host:whoami.example.com"
|
||||
- "traefik.http.services.whoami.loadbalancer.server.port=80"
|
||||
- "traefik.frontend.auth.forward.address=http://traefik-forward-auth:4181"
|
||||
- "traefik.frontend.auth.forward.authResponseHeaders=X-Forwarded-User"
|
||||
- "traefik.frontend.auth.forward.trustForwardHeader=true"
|
||||
|
||||
# traefikv2
|
||||
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
|
||||
- "traefik.http.routers.whoami.entrypoints=https"
|
||||
- "traefik.http.services.whoami.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.whoami.middlewares=forward-auth"
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
external: true
|
||||
```
|
||||
|
||||
## Serving
|
||||
|
||||
### Launch
|
||||
|
||||
Deploy dex with `docker stack deploy dex -c /var/data/dex/dex.yml`, to launch dex, and then deploy Traefik Forward Auth with `docker stack deploy traefik-forward-auth -c /var/data/traefik-forward-auth/traefik-forward-auth.yml`
|
||||
|
||||
Once you redeploy traefik-forward-auth with the above, it **should** use dex as an OIDC provider, authenticating you against the `staticPasswords` username and hashed password described in `config.yml` above.
|
||||
|
||||
### Test
|
||||
|
||||
Browse to https://whoami.example.com (_obviously, customized for your domain and having created a DNS record_), and all going according to plan, you'll be redirected to a CoreOS Dex login. Once successfully logged in, you'll be directed to the basic whoami page :thumbsup:
|
||||
|
||||
### Protect services
|
||||
|
||||
To protect any other service, ensure the service itself is exposed by Traefik (_if you were previously using an oauth_proxy for this, you may have to migrate some labels from the oauth_proxy serivce to the service itself_). Add the following label:
|
||||
|
||||
```yaml
|
||||
- "traefik.http.routers.radarr.middlewares=forward-auth"
|
||||
```
|
||||
|
||||
And re-deploy your services :)
|
||||
|
||||
## Summary
|
||||
|
||||
What have we achieved? By adding an additional label to any service, we can secure any service behind our (static) OIDC provider, with minimal processing / handling overhead.
|
||||
|
||||
!!! summary "Summary"
|
||||
Created:
|
||||
|
||||
* [X] Traefik-forward-auth configured to authenticate against Dex (static)
|
||||
|
||||
[^1]: You can remove the `whoami` container once you know Traefik Forward Auth is working properly
|
||||
|
||||
--8<-- "recipe-footer.md"
|
||||
130
manuscript/ha-docker-swarm/traefik-forward-auth/google.md
Normal file
130
manuscript/ha-docker-swarm/traefik-forward-auth/google.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Traefik Forward Auth using Google
|
||||
|
||||
[Traefik Forward Auth](/ha-docker-swarm/traefik-forward-auth/) is incredibly useful to secure services with an additional layer of authentication, provided by an OIDC-compatible provider. The simplest possible provider is a self-hosted instance of [Dex][dex], configured with a static username and password. This is not much use if you want to provide "normies" access to your services though - a better solution would be to validate their credentials against an existing trusted public source.
|
||||
|
||||
This recipe will illustrate how to point Traefik Forward Auth to Google, confirming that the requestor has a valid Google account (*and that said account is permitted to access your services!*)
|
||||
|
||||
--8<-- "recipe-tfa-ingredients.md"
|
||||
|
||||
## Preparation
|
||||
|
||||
### Obtain OAuth credentials
|
||||
|
||||
#### TL;DR
|
||||
|
||||
Log into https://console.developers.google.com/, create a new project then search for and select "**Credentials**" in the search bar.
|
||||
|
||||
Fill out the "OAuth Consent Screen" tab, and then click, "**Create Credentials**" > "**OAuth client ID**". Select "**Web Application**", fill in the name of your app, skip "**Authorized JavaScript origins**" and fill "**Authorized redirect URIs**" with either all the domains you will allow authentication from, appended with the url-path (*e.g. https://radarr.example.com/_oauth, https://radarr.example.com/_oauth, etc*), or if you don't like frustration, use a "auth host" URL instead, like "*https://auth.example.com/_oauth*" (*see below for details*)
|
||||
|
||||
#### Monkey see, monkey do 🙈
|
||||
|
||||
Here's a [screencast I recorded](https://static.funkypenguin.co.nz/2021/screencast_2021-01-29_22-29-33.gif) of the OIDC credentias setup in Google Developer Console
|
||||
|
||||
!!! tip
|
||||
Store your client ID and secret safely - you'll need them for the next step.
|
||||
|
||||
### Prepare environment
|
||||
|
||||
Create `/var/data/config/traefik-forward-auth/traefik-forward-auth.env` as follows:
|
||||
|
||||
```
|
||||
PROVIDERS_GOOGLE_CLIENT_ID=<your client id>
|
||||
PROVIDERS_GOOGLE_CLIENT_SECRET=<your client secret>
|
||||
SECRET=<a random string, make it up>
|
||||
# comment out AUTH_HOST if you'd rather use individual redirect_uris (slightly less complicated but more work)
|
||||
AUTH_HOST=auth.example.com
|
||||
COOKIE_DOMAINS=example.com
|
||||
WHITELIST=you@yourdomain.com, me@mydomain.com
|
||||
```
|
||||
|
||||
### Prepare the docker service config
|
||||
|
||||
Create `/var/data/config/traefik-forward-auth/traefik-forward-auth.yml` as follows:
|
||||
|
||||
```
|
||||
traefik-forward-auth:
|
||||
image: thomseddon/traefik-forward-auth:2.1.0
|
||||
env_file: /var/data/config/traefik-forward-auth/traefik-forward-auth.env
|
||||
networks:
|
||||
- traefik_public
|
||||
deploy:
|
||||
labels # you only need these if you're using an auth host
|
||||
# traefik
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik_public
|
||||
|
||||
# traefikv1
|
||||
- "traefik.port=4181"
|
||||
- "traefik.frontend.rule=Host:auth.example.com"
|
||||
- "traefik.frontend.auth.forward.address=http://traefik-forward-auth:4181"
|
||||
- "traefik.frontend.auth.forward.trustForwardHeader=true"
|
||||
|
||||
# traefikv2
|
||||
- "traefik.docker.network=traefik_public"
|
||||
- "traefik.http.routers.auth.rule=Host(`auth.example.com`)"
|
||||
- "traefik.http.routers.auth.entrypoints=https"
|
||||
- "traefik.http.routers.auth.tls=true"
|
||||
- "traefik.http.routers.auth.tls.domains[0].main=example.com"
|
||||
- "traefik.http.routers.auth.tls.domains[0].sans=*.example.com"
|
||||
- "traefik.http.routers.auth.tls.certresolver=main"
|
||||
- "traefik.http.routers.auth.service=auth@docker"
|
||||
- "traefik.http.services.auth.loadbalancer.server.port=4181"
|
||||
- "traefik.http.middlewares.forward-auth.forwardauth.address=http://traefik-forward-auth:4181"
|
||||
- "traefik.http.middlewares.forward-auth.forwardauth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
|
||||
- "traefik.http.routers.auth.middlewares=forward-auth"
|
||||
```
|
||||
|
||||
If you're not confident that forward authentication is working, add a simple "whoami" test container to the above .yml, to help debug traefik forward auth, before attempting to add it to a more complex container.
|
||||
|
||||
```
|
||||
# This simply validates that traefik forward authentication is working
|
||||
whoami:
|
||||
image: containous/whoami
|
||||
networks:
|
||||
- traefik_public
|
||||
deploy:
|
||||
labels:
|
||||
# traefik
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik_public
|
||||
|
||||
# traefikv1
|
||||
- traefik.frontend.rule=Host:whoami.example.com
|
||||
- "traefik.http.services.linx.loadbalancer.server.port=80"
|
||||
- traefik.frontend.auth.forward.address=http://traefik-forward-auth:4181
|
||||
- traefik.frontend.auth.forward.authResponseHeaders=X-Forwarded-User
|
||||
- traefik.frontend.auth.forward.trustForwardHeader=true
|
||||
|
||||
# traefikv2
|
||||
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
|
||||
- "traefik.http.routers.whoami.entrypoints=https"
|
||||
- "traefik.http.services.whoami.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.whoami.middlewares=forward-auth" # this line enforces traefik-forward-auth
|
||||
|
||||
```
|
||||
|
||||
--8<-- "premix-cta.md"
|
||||
|
||||
## Serving
|
||||
|
||||
### Launch
|
||||
|
||||
Deploy traefik-forward-auth with ```docker stack deploy traefik-forward-auth -c /var/data/traefik-forward-auth/traefik-forward-auth.yml```
|
||||
|
||||
### Test
|
||||
|
||||
Browse to https://whoami.example.com (*obviously, customized for your domain and having created a DNS record*), and all going according to plan, you should be redirected to a Google login. Once successfully logged in, you'll be directed to the basic whoami page.
|
||||
|
||||
## Summary
|
||||
|
||||
What have we achieved? By adding an additional three simple labels to any service, we can secure any service behind our choice of OAuth provider, with minimal processing / handling overhead.
|
||||
|
||||
!!! summary "Summary"
|
||||
Created:
|
||||
|
||||
* [X] Traefik-forward-auth configured to authenticate against an OIDC provider
|
||||
|
||||
[^1]: Be sure to populate `WHITELIST` in `traefik-forward-auth.env`, else you'll happily be granting **any** authenticated Google account access to your services!
|
||||
|
||||
--8<-- "recipe-footer.md"
|
||||
@@ -2,36 +2,10 @@
|
||||
|
||||
While the [Traefik Forward Auth](/ha-docker-swarm/traefik-forward-auth/) recipe demonstrated a quick way to protect a set of explicitly-specified URLs using OIDC credentials from a Google account, this recipe will illustrate how to use your own KeyCloak instance to secure **any** URLs within your DNS domain.
|
||||
|
||||
## Ingredients
|
||||
|
||||
!!! Summary
|
||||
Existing:
|
||||
|
||||
* [X] [KeyCloak](/recipes/keycloak/) recipe deployed successfully, with a [local user](/recipes/keycloak/create-user/) and an [OIDC client](/recipes/keycloak/setup-oidc-provider/)
|
||||
|
||||
New:
|
||||
|
||||
* [ ] DNS entry for your auth host (*"auth.yourdomain.com" is a good choice*), pointed to your [keepalived](/ha-docker-swarm/keepalived/) IP
|
||||
--8<-- "recipe-tfa-ingredients.md"
|
||||
|
||||
## Preparation
|
||||
|
||||
### What is AuthHost mode
|
||||
|
||||
Under normal OIDC auth, you have to tell your auth provider which URLs it may redirect an authenticated user back to, post-authentication. This is a security feture of the OIDC spec, preventing a malicious landing page from capturing your session and using it to impersonate you. When you're securing many URLs though, explicitly listing them can be a PITA.
|
||||
|
||||
[@thomaseddon's traefik-forward-auth](https://github.com/thomseddon/traefik-forward-auth) includes an ingenious mechanism to simulate an "_auth host_" in your OIDC authentication, so that you can protect an unlimited amount of DNS names (_with a common domain suffix_), without having to manually maintain a list.
|
||||
|
||||
#### How does it work?
|
||||
|
||||
Say you're protecting **radarr.example.com**. When you first browse to **https://radarr.example.com**, Traefik forwards your session to traefik-forward-auth, to be authenticated. Traefik-forward-auth redirects you to your OIDC provider's login (_KeyCloak, in this case_), but instructs the OIDC provider to redirect a successfully authenticated session **back** to **https://auth.example.com/_oauth**, rather than to **https://radarr.example.com/_oauth**.
|
||||
|
||||
When you successfully authenticate against the OIDC provider, you are redirected to the "_redirect_uri_" of https://auth.example.com. Again, your request hits Traefik, whichforwards the session to traefik-forward-auth, which **knows** that you've just been authenticated (_cookies have a role to play here_). Traefik-forward-auth also knows the URL of your **original** request (_thanks to the X-Forwarded-Whatever header_). Traefik-forward-auth redirects you to your original destination, and everybody is happy.
|
||||
|
||||
This clever workaround only works under 2 conditions:
|
||||
|
||||
1. Your "auth host" has the same domain name as the hosts you're protecting (_i.e., auth.example.com protecting radarr.example.com_)
|
||||
2. You explictly tell traefik-forward-auth to use a cookie authenticating your **whole** domain (_i.e. example.com_)
|
||||
|
||||
### Setup environment
|
||||
|
||||
Create `/var/data/config/traefik/traefik-forward-auth.env` as follows (_change "master" if you created a different realm_):
|
||||
@@ -109,7 +83,7 @@ And re-deploy your services :)
|
||||
What have we achieved? By adding an additional three simple labels to any service, we can secure any service behind our KeyCloak OIDC provider, with minimal processing / handling overhead.
|
||||
|
||||
!!! summary "Summary"
|
||||
Created:
|
||||
Created:
|
||||
|
||||
* [X] Traefik-forward-auth configured to authenticate against KeyCloak
|
||||
|
||||
|
||||
@@ -13,15 +13,20 @@ To deal with these gaps, we need a front-end load-balancer, and in this design,
|
||||
|
||||

|
||||
|
||||
!!! tip
|
||||
In 2021, this recipe was updated for Traefik v2. There's really no reason to be using Traefikv1 anymore ;)
|
||||
|
||||
## Ingredients
|
||||
|
||||
!!! summary "You'll need"
|
||||
Existing
|
||||
!!! summary "Ingredients"
|
||||
Already deployed:
|
||||
|
||||
* [X] [Docker swarm cluster](/ha-docker-swarm/design/) with [persistent shared storage](/ha-docker-swarm/shared-storage-ceph)
|
||||
|
||||
New
|
||||
* [X] [Docker swarm cluster](/ha-docker-swarm/design/) with [persistent shared storage](/ha-docker-swarm/shared-storage-ceph.md)
|
||||
* [X] [Traefik](/ha-docker-swarm/traefik) configured per design
|
||||
* [X] DNS entry for the hostname you intend to use (*or a wildcard*), pointed to your [keepalived](/ha-docker-swarm/keepalived/) IP
|
||||
|
||||
New:
|
||||
|
||||
* [ ] Access to update your DNS records for manual/automated [LetsEncrypt](https://letsencrypt.org/docs/challenge-types/) DNS-01 validation, or ingress HTTP/HTTPS for HTTP-01 validation
|
||||
|
||||
## Preparation
|
||||
@@ -30,53 +35,58 @@ To deal with these gaps, we need a front-end load-balancer, and in this design,
|
||||
|
||||
While it's possible to configure traefik via docker command arguments, I prefer to create a config file (`traefik.toml`). This allows me to change traefik's behaviour by simply changing the file, and keeps my docker config simple.
|
||||
|
||||
Create `/var/data/traefikv1/traefik.toml` as follows:
|
||||
Create `/var/data/traefikv2/traefik.toml` as follows:
|
||||
|
||||
```
|
||||
checkNewVersion = true
|
||||
defaultEntryPoints = ["http", "https"]
|
||||
[global]
|
||||
checkNewVersion = true
|
||||
|
||||
# This section enable LetsEncrypt automatic certificate generation / renewal
|
||||
[acme]
|
||||
email = "<your LetsEncrypt email address>"
|
||||
storage = "acme.json" # or "traefik/acme/account" if using KV store
|
||||
entryPoint = "https"
|
||||
acmeLogging = true
|
||||
onDemand = true
|
||||
OnHostRule = true
|
||||
# Enable the Dashboard
|
||||
[api]
|
||||
dashboard = true
|
||||
|
||||
# Request wildcard certificates per https://docs.traefik.io/configuration/acme/#wildcard-domains
|
||||
[[acme.domains]]
|
||||
main = "*.example.com"
|
||||
sans = ["example.com"]
|
||||
# Write out Traefik logs
|
||||
[log]
|
||||
level = "INFO"
|
||||
filePath = "/traefik.log"
|
||||
|
||||
# Redirect all HTTP to HTTPS (why wouldn't you?)
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
[entryPoints.http.redirect]
|
||||
entryPoint = "https"
|
||||
[entryPoints.https]
|
||||
# Redirect to HTTPS (why wouldn't you?)
|
||||
[entryPoints.http.http.redirections.entryPoint]
|
||||
to = "https"
|
||||
scheme = "https"
|
||||
|
||||
[entryPoints.https]
|
||||
address = ":443"
|
||||
[entryPoints.https.tls]
|
||||
[entryPoints.https.http.tls]
|
||||
certResolver = "main"
|
||||
|
||||
[web]
|
||||
address = ":8080"
|
||||
watch = true
|
||||
# Let's Encrypt
|
||||
[certificatesResolvers.main.acme]
|
||||
email = "batman@example.com"
|
||||
storage = "acme.json"
|
||||
# uncomment to use staging CA for testing
|
||||
# caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
[certificatesResolvers.main.acme.dnsChallenge]
|
||||
provider = "route53"
|
||||
# Uncomment to use HTTP validation, like a caveman!
|
||||
# [certificatesResolvers.main.acme.httpChallenge]
|
||||
# entryPoint = "http"
|
||||
|
||||
[docker]
|
||||
endpoint = "tcp://127.0.0.1:2375"
|
||||
domain = "example.com"
|
||||
watch = true
|
||||
swarmmode = true
|
||||
# Docker Traefik provider
|
||||
[providers.docker]
|
||||
endpoint = "unix:///var/run/docker.sock"
|
||||
swarmMode = true
|
||||
watch = true
|
||||
```
|
||||
|
||||
### Prepare the docker service config
|
||||
|
||||
!!! tip
|
||||
"We'll want an overlay network, independent of our traefik stack, so that we can attach/detach all our other stacks (including traefik) to the overlay network. This way, we can undeploy/redepoly the traefik stack without having to bring every other stack first!" - voice of experience
|
||||
"We'll want an overlay network, independent of our traefik stack, so that we can attach/detach all our other stacks (including traefik) to the overlay network. This way, we can undeploy/redepoly the traefik stack without having to bring down every other stack first!" - voice of hard-won experience
|
||||
|
||||
Create `/var/data/config/traefik/traefik.yml` as follows:
|
||||
Create `/var/data/config/traefikv2/traefikv2.yml` as follows:
|
||||
|
||||
```
|
||||
version: "3.2"
|
||||
@@ -105,18 +115,18 @@ networks:
|
||||
|
||||
--8<-- "premix-cta.md"
|
||||
|
||||
Create `/var/data/config/traefik/traefik-app.yml` as follows:
|
||||
Create `/var/data/config/traefikv2/traefikv2.yml` as follows:
|
||||
|
||||
```
|
||||
version: "3.2"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v1.7.16
|
||||
command: --web --docker --docker.swarmmode --docker.watch --docker.domain=example.com --logLevel=DEBUG
|
||||
app:
|
||||
image: traefik:v2.4
|
||||
env_file: /var/data/config/traefikv2/traefikv2.env
|
||||
# Note below that we use host mode to avoid source nat being applied to our ingress HTTP/HTTPS sessions
|
||||
# Without host mode, all inbound sessions would have the source IP of the swarm nodes, rather than the
|
||||
# original source IP, which would impact logging. If you don't care about this, you can expose ports the
|
||||
# original source IP, which would impact logging. If you don't care about this, you can expose ports the
|
||||
# "minimal" way instead
|
||||
ports:
|
||||
- target: 80
|
||||
@@ -132,21 +142,30 @@ services:
|
||||
protocol: tcp
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /var/data/traefikv1:/etc/traefik
|
||||
- /var/data/traefikv1/traefik.log:/traefik.log
|
||||
- /var/data/traefikv1/acme.json:/acme.json
|
||||
- /var/data/config/traefikv2:/etc/traefik
|
||||
- /var/data/traefikv2/traefik.log:/traefik.log
|
||||
- /var/data/traefikv2/acme.json:/acme.json
|
||||
networks:
|
||||
- traefik_public
|
||||
# Global mode makes an instance of traefik listen on _every_ node, so that regardless of which
|
||||
# node the request arrives on, it'll be forwarded to the correct backend service.
|
||||
deploy:
|
||||
labels:
|
||||
- "traefik.enable=false"
|
||||
mode: global
|
||||
labels:
|
||||
- "traefik.docker.network=traefik_public"
|
||||
- "traefik.http.routers.api.rule=Host(`traefik.example.com`)"
|
||||
- "traefik.http.routers.api.entrypoints=https"
|
||||
- "traefik.http.routers.api.tls.domains[0].main=example.com"
|
||||
- "traefik.http.routers.api.tls.domains[0].sans=*.example.com"
|
||||
- "traefik.http.routers.api.tls=true"
|
||||
- "traefik.http.routers.api.tls.certresolver=main"
|
||||
- "traefik.http.routers.api.service=api@internal"
|
||||
- "traefik.http.services.dummy.loadbalancer.server.port=9999"
|
||||
|
||||
# uncomment this to enable forward authentication on the traefik api/dashboard
|
||||
#- "traefik.http.routers.api.middlewares=forward-auth"
|
||||
placement:
|
||||
constraints: [node.role == manager]
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
networks:
|
||||
traefik_public:
|
||||
@@ -156,10 +175,10 @@ networks:
|
||||
Docker won't start a service with a bind-mount to a non-existent file, so prepare an empty acme.json and traefik.log (_with the appropriate permissions_) by running:
|
||||
|
||||
```
|
||||
touch /var/data/traefikv1/acme.json
|
||||
touch /var/data/traefikv1/traefik.log
|
||||
chmod 600 /var/data/traefikv1/acme.json
|
||||
chmod 600 /var/data/traefikv1/traefik.log
|
||||
touch /var/data/traefikv2/acme.json
|
||||
touch /var/data/traefikv2/traefik.log
|
||||
chmod 600 /var/data/traefikv2/acme.json
|
||||
chmod 600 /var/data/traefikv2/traefik.log
|
||||
```
|
||||
|
||||
!!! warning
|
||||
@@ -182,26 +201,26 @@ Creating service traefik_scratch
|
||||
[root@kvm ~]#
|
||||
```
|
||||
|
||||
Now deploy the traefik appliation itself (*which will attach to the overlay network*) by running `docker stack deploy traefik-app -c /var/data/config/traefik/traefik-app.yml`
|
||||
Now deploy the traefik application itself (*which will attach to the overlay network*) by running `docker stack deploy traefikv2 -c /var/data/config/traefikv2/traefikv2.yml`
|
||||
|
||||
```
|
||||
[root@kvm ~]# docker stack deploy traefik-app -c traefik-app.yml
|
||||
Creating service traefik-app_app
|
||||
[root@kvm ~]# docker stack deploy traefik-app -c traefikv2.yml
|
||||
Creating service traefikv2_app
|
||||
[root@kvm ~]#
|
||||
```
|
||||
|
||||
Confirm traefik is running with `docker stack ps traefik-app`:
|
||||
Confirm traefik is running with `docker stack ps traefikv2`:
|
||||
|
||||
```
|
||||
[root@kvm ~]# docker stack ps traefik-app
|
||||
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
|
||||
74uipz4sgasm traefik-app_app.t4vcm8siwc9s1xj4c2o4orhtx traefik:alpine kvm.funkypenguin.co.nz Running Running 33 seconds ago *:443->443/tcp,*:80->80/tcp
|
||||
[root@kvm ~]#
|
||||
root@raphael:~# docker stack ps traefikv2
|
||||
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
|
||||
lmvqcfhap08o traefikv2_app.dz178s1aahv16bapzqcnzc03p traefik:v2.4 donatello Running Running 2 minutes ago *:443->443/tcp,*:80->80/tcp
|
||||
root@raphael:~#
|
||||
```
|
||||
|
||||
### Check Traefik Dashboard
|
||||
|
||||
You should now be able to access[^1] your traefik instance on http://<node IP\>:8080 - It'll look a little lonely currently (*below*), but we'll populate it as we add recipes :)
|
||||
You should now be able to access[^1] your traefik instance on **https://traefik.<your domain\>** (*if your LetsEncrypt certificate is working*), or **http://<node IP\>:8080** (*if it's not*)- It'll look a little lonely currently (*below*), but we'll populate it as we add recipes :grin:
|
||||
|
||||

|
||||
|
||||
|
||||
Reference in New Issue
Block a user