Guide for installing Outline with Authelia as an OpenID provider
This is an incomplete guide on how to self-host Outline and take advantage of their recently support for OpenID provider as Authelia recent Beta support for OAuth2 flow. This article should be a good starting point for people to get something working.
Warning!
I am going to generate some certificates because I wanted to use a non-offical TLD (Top-Level Domain). It is not a good practice. However homelabing is always a good opportunity for experimenting so I will detail how I have proceed to do so as best as I can.
Before starting
We are going to use with Docker and Docker Compose. Be sure to have those installed before going further.
Building Outline image
Outline latest release (v0.59.0) doesn’t contain a needed fix for OAuth2 to be working correctly (https://github.com/outline/outline/issues/2663). We are going first to build our image. If by the time you are reading this a version higher than v0.59.0 is out, you can skip this and use their docker image.
$ git clone https://github.com/outline/outline.git
$ cd outline
$ docker build -t outline:main .
Setting up Authelia
A word about Authelia
Authelia is an open-source authentication server. It allows you to restrict access to your services to authenticated users. If you are still not sure that it is what you need you can check Techno Tim video on it : https://www.youtube.com/watch?v=u6H-Qwf4nZA
Recently, Authelia developers have added Beta support for OpenID Connect (https://www.authelia.com/docs/configuration/identity-providers/oidc.html). This is what we are going to use to log in to our Outline wiki because we like living on the edge.
The docker-compose file
This docker-compose.yml
file is inspired by the local example used in Authelia.
---
version: '3.3'
networks:
net:
driver: bridge
services:
authelia:
image: authelia/authelia
container_name: authelia
volumes:
- ./authelia:/config
networks:
- net
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.authelia.rule=Host(`authelia.example.com`)'
- 'traefik.http.routers.authelia.entrypoints=https'
- 'traefik.http.routers.authelia.tls=true'
- 'traefik.http.routers.authelia.tls.options=default'
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://authelia.evil.corp' # yamllint disable-line rule:line-length
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email' # yamllint disable-line rule:line-length
expose:
- 9091
restart: unless-stopped
healthcheck:
disable: true
environment:
- TZ=Europe/Paris
traefik:
image: traefik:2.4
container_name: traefik
volumes:
- ./traefik:/etc/traefik
- /var/run/docker.sock:/var/run/docker.sock
networks:
net:
aliases:
- traefik.evil.corp
- authelia.evil.corp
- outline.evil.corp
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.api.rule=Host(`traefik.evil.corp`)'
- 'traefik.http.routers.api.entrypoints=https'
- 'traefik.http.routers.api.service=api@internal'
- 'traefik.http.routers.api.tls=true'
- 'traefik.http.routers.api.tls.options=default'
- 'traefik.http.routers.api.middlewares=authelia@docker'
ports:
- 80:80
- 443:443
command:
- '--api'
- '--providers.docker=true'
- '--providers.docker.exposedByDefault=false'
- '--providers.file.filename=/etc/traefik/certificates.yml'
- '--entrypoints.http=true'
- '--entrypoints.http.address=:80'
- '--entrypoints.http.http.redirections.entrypoint.to=https'
- '--entrypoints.http.http.redirections.entrypoint.scheme=https'
- '--entrypoints.https=true'
- '--entrypoints.https.address=:443'
- '--log=true'
- '--log.level=DEBUG'
...
The trick is the use of the middleware authelia@docker
.
Note:
You might wonder why we have added the network aliases in traefik ?
networks:
net:
aliases:
- traefik.evil.corp
- authelia.evil.corp
- outline.evil.corp
It is because outline container will try to talk to authelia container directly during the authentication process. The service wull need resolve the name (that are using not official TLD) to our traefik container that will then dispatch the message to authelia container. Probably that using the “host” mod instead of specifying our own network would have made it easier. It was an interesting problem to explore though.
Configuring Authelia
In the configuration file we are going to need an issuer key. We can generate it now using their command line.
$ docker run -u "$(id -u):$(id -g)" -v "$(pwd)":/keys authelia/authelia:latest authelia rsa generate --dir /keys
Lets create a authelia
folder to store our configuration file.
./authelia/configuration.yml
---
###############################################################
# Authelia configuration #
###############################################################jwt_secret: a_very_important_secret
default_redirection_url: https://traefik.evil.corpserver:
host: 0.0.0.0
port: 9091log:
level: debugtotp:
issuer: authelia.comauthentication_backend:
file:
path: /config/users_database.ymlaccess_control:
default_policy: deny
rules:
- domain: traefik.evil.corp
policy: one_factorsession:
name: authelia_session
secret: unsecure_session_secret
expiration: 3600 # 1 hour
inactivity: 300 # 5 minutes
domain: evil.corp # Should match whatever your root protected domain isregulation:
max_retries: 3
find_time: 120
ban_time: 300storage:
local:
path: /config/db.sqlite3notifier:
filesystem:
filename: /config/notification.txt
identity_providers:
oidc:
hmac_secret: this_is_a_secret_abc123abc123abc
issuer_private_key: |
-----BEGIN RSA PRIVATE KEY-----
FilLth1SH3RE...
-----END RSA PRIVATE KEY-----
access_token_lifespan: 1h
authorize_code_lifespan: 1m
id_token_lifespan: 1h
refresh_token_lifespan: 90m
enable_client_debug_messages: false
clients:
- id: outline
description: Outline
secret: this_is_a_secret
public: false
authorization_policy: one_factor
audience: []
scopes:
- openid
- groups
- profile
redirect_uris:
- https://outline.evil.corp/auth/oidc.callback
grant_types:
- refresh_token
- authorization_code
response_types:
- code
response_modes:
- form_post
- query
- fragment
userinfo_signing_algorithm: none
...
Obviously you should generate your own secrets and replace the RSA issuer key with the one generated in the previous step.
The OAuth ID should probably be a more random value and not just this.
clients:
- id: outline
In this article we are going to use a simple YAML file as our user backend to keep it simple.
authentication_backend:
file:
path: /config/users_database.yml
The service is also going to use sqlite to quickly store value as needed. Other options are available (https://www.authelia.com/docs/configuration/storage/)
storage:
local:
path: /config/db.sqlite3
The notifier section let’s us just write what would be the verification email to a notification file.
notifier:
filesystem:
filename: /config/notification.txt
Now let’s create our users “database” aka our YAML file.
./authelia/users_database.yml
---
###############################################################
# Users Database #
################################################################ This file can be used if you do not have an LDAP set up.# List of users
users:
lola:
displayname: "Sleepy Ramen"
password: <fill-this-here>
email: lola@evil.corp
groups:
- admins
- dev
...
To generate the password you can once again use authelia docker.
$ docker run authelia/authelia authelia hash-password 1234
Password hash: $argon2id$v=19$m=65536,t=1,p=8$OTRMVTlMUDFsZm83QTBVRQ$E7CtJgRA706UlBhUu7Xt+sw3koFFbpGIrhM9F2a60vI
Replace <fill-this-here>
with the hashed password returned.
Generating our CA certificate and our wildcard certificate
I wouldn’t recommend that part for your homelab. You should probably just stick with LetsEncrypt certificate. However if like me you want to use the TLD .corp (https://tools.ietf.org/id/draft-chapin-rfc2606bis-00.html#new) just because you can, the following section will help you with that.
First we are going to generate our CA (Certificate Authority) key pair. So we will act as our own certificate authority.
$ openssl genrsa -des3 -out evil-ca.key 2048
$ openssl req -x509 -new -nodes -key evil-ca.key -sha256 -days 1825 -out evil-ca.pem
You can check what is in your .pem file with the following command:
$ openssl x509 -noout -text -in evil-ca.pem
Now let’s create the key for our wildcard certificate.
$ openssl genrsa -des3 -out evil.corp.key 2048
We can then create a request for our CA.
$ openssl req -new -key evil.corp.key -out evil.corp.csr
We want to specify that our certificate should be used for TLS Web Server Authentication
so we create an .ext file that will help us specify this.
./evil.corp.ext
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names[alt_names]
DNS.1 = *.evil.corp
With this specification, we can sign our request with using our CA key.
$ openssl x509 -req -in evil.corp.csr -CA evil-ca.pem -CAkey evil-ca.key -CAcreateserial -out evil.corp.crt -days 825 -sha256 -extfile evil.corp.ext
Check your newly created certificate.
$ openssl x509 -noout -text -in evil.corp.crt
There is many configuration possible. The all thing is a bit complicated but overall really interesting to dive in. You can find more useful information on this website : https://jamielinux.com/docs/openssl-certificate-authority/index.html
Lets put everything together for traefik to find this.
$ mkdir traefik
$ cd traefik
$ mkdir certs
$ touch certificates.yml
$ mv ../../evil.corp.key ./certs/key.pem
$ mv ../../evil.corp.crt ./certs/cert.pem
$ cd ../..
$ mkdir ca
$ cd ca
$ mv ../evil-ca.crt .
Our certificates.yml file
---
tls:
certificates:
- certFile: /etc/traefik/certs/cert.pem
keyFile: /etc/traefik/certs/key.pem
...
If you have choose to do it this way you probably will want to add your domain name to your /etc/hosts so that it will point to your server on your local network.
Setting up Outline
Our docker-compose.yml is incomplete. It is missing our outline service and the completing service for it to run.
Completing docker-compose.yml
redis:
image: redis
container_name: redis
networks:
- netpostgres:
image: postgres
container_name: postgres
networks:
- net
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: outlines3:
image: lphoward/fake-s3
container_name: fake-s3
networks:
- net
volumes:
- ./fakes3:/fakes3_rootoutline:
image: outline:main
container_name: outline
command: sh -c "yarn sequelize:migrate --env=production-ssl-disabled && yarn start"
networks:
- net
expose:
- 3000
volumes:
- ./ca:/ca
environment:
- NODE_EXTRA_CA_CERTS=/ca/evil-ca.crt
env_file:
- .env
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.outline.rule=Host(`outline.evil.corp`)'
- 'traefik.http.routers.outline.entrypoints=https'
- 'traefik.http.routers.outline.tls=true'
- 'traefik.http.routers.outline.tls.options=default'
depends_on:
- postgres
- redis
- s3
Outline required a S3 access to work but for now we are using a fake one. You can later replace it with Minio.
Configuring Outline
The outline container does load a .env
file that we will now create.
./.env
# 👋 Welcome, we're glad you're setting up an installation of Outline. Copy this
# file to .env or set the variables in your local environment manually. For
# development with docker this should mostly work out of the box other than
# setting the Slack keys and the SECRET_KEY.
# –––––––––––––––– REQUIRED ––––––––––––––––
# Generate a hex-encoded 32-byte random key. You should use `openssl rand -hex 32`
# in your terminal to generate a random value.
SECRET_KEY=generate_a_new_key
# Generate a unique random key. The format is not important but you could still use
# `openssl rand -hex 32` in your terminal to produce this.
UTILS_SECRET=generate_a_new_key
# For production point these at your databases, in development the default
# should work out of the box.
DATABASE_URL=postgres://user:pass@postgres:5432/outline
DATABASE_URL_TEST=postgres://user:pass@postres:5432/outline-test
# Uncomment this to disable SSL for connecting to Postgres
# PGSSLMODE=disable
REDIS_URL=redis://redis:6379
# URL should point to the fully qualified, publicly accessible URL. If using a
# proxy the port in URL and PORT may be different.
URL=https://outline.evil.corp
PORT=3000
# See [documentation](docs/SERVICES.md) on running a separate collaboration
# server, for normal operation this does not need to be set.
COLLABORATION_URL=
# To support uploading of images for avatars and document attachments an
# s3-compatible storage must be provided. AWS S3 is recommended for redundency
# however if you want to keep all file storage local an alternative such as
# minio (https://github.com/minio/minio) can be used.
# A more detailed guide on setting up S3 is available here:
# => https://wiki.generaloutline.com/share/125de1cc-9ff6-424b-8415-0d58c809a40f
#
AWS_ACCESS_KEY_ID=get_a_key_from_aws
AWS_SECRET_ACCESS_KEY=get_the_secret_of_above_key
AWS_REGION=xx-xxxx-x
AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569
AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
AWS_S3_UPLOAD_MAX_SIZE=26214400
AWS_S3_FORCE_PATH_STYLE=true
AWS_S3_ACL=private
# –––––––––––––– AUTHENTICATION ––––––––––––––
# Third party signin credentials, at least ONE OF EITHER Google, Slack,
# or Microsoft is required for a working installation or you'll have no sign-in
# options.
# To configure Slack auth, you'll need to create an Application at
# => https://api.slack.com/apps
#
# When configuring the Client ID, add a redirect URL under "OAuth & Permissions":
# https://<URL>/auth/slack.callback
SLACK_KEY=
SLACK_SECRET=
# To configure Google auth, you'll need to create an OAuth Client ID at
# => https://console.cloud.google.com/apis/credentials
#
# When configuring the Client ID, add an Authorized redirect URI:
# https://<URL>/auth/google.callback
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# To configure Microsoft/Azure auth, you'll need to create an OAuth Client. See
# the guide for details on setting up your Azure App:
# => https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4
AZURE_CLIENT_ID=
AZURE_CLIENT_SECRET=
AZURE_RESOURCE_APP_ID=
# To configure generic OIDC auth, you'll need some kind of identity provider.
# See documentation for whichever IdP you use to acquire the following info:
# Redirect URI is https://<URL>/auth/oidc.callback
OIDC_CLIENT_ID=outline
OIDC_CLIENT_SECRET=this_is_a_secret
OIDC_AUTH_URI=https://authelia.evil.corp/api/oidc/authorize
OIDC_TOKEN_URI=https://authelia.evil.corp/api/oidc/token
OIDC_USERINFO_URI=https://authelia.evil.corp/api/oidc/userinfo
# Display name for OIDC authentication
OIDC_DISPLAY_NAME=Evil, corp.
# Space separated auth scopes.
OIDC_SCOPES="openid profile email"
There is more optional settings available that you can check here : https://github.com/outline/outline/blob/main/.env.sample
Of course, you will need to generate your secrets.
docker-compose up
We have everything setup and we can now launch our containers.
$ docker-compose up -d
Now try to access https://outline.evil.corp.
You will need to accept the self-signed certificate as your browser will warn you of the risk.
It is a bit tedious to get together but also a good opportunity to learn about certificates and OAuth2 workflow. There is more to do like replacing our fake s3 with Minio.
Special thanks to Authelia and Outline developpers for their projects.
This article was inspired by the “Deploying Outline Wiki” article that explained how to use Outline with Keycloak.