Guide for installing Outline with Authelia as an OpenID provider

Rigaut-Luczak Lola
8 min readNov 14, 2021

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.

screenshots of the Outline interface after logging in

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.corp
server:
host: 0.0.0.0
port: 9091
log:
level: debug
totp:
issuer: authelia.com
authentication_backend:
file:
path: /config/users_database.yml
access_control:
default_policy: deny
rules:
- domain: traefik.evil.corp
policy: one_factor
session:
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 is
regulation:
max_retries: 3
find_time: 120
ban_time: 300
storage:
local:
path: /config/db.sqlite3
notifier:
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
- email
- 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:
- net
postgres:
image: postgres
container_name: postgres
networks:
- net
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: outline
s3:
image: lphoward/fake-s3
container_name: fake-s3
networks:
- net
volumes:
- ./fakes3:/fakes3_root
outline:
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.

--

--

Rigaut-Luczak Lola

Ingénieure en informatique. Intéressée par Bitcoin depuis 2013. Ne comprend pas la blockchain.