Ochrona własnego intranetu za pomocą rozwiązania Authelia i historia odejścia od web serwera Caddy
Table of Contents
Jak większość osób mających małą sieć hostującą laba z eksperymentami i kilka prywatnych rozwiązań, które nie powinny być otwarte dla całego świata z szerokiego wachlarza powodów (od bezpieczeństwa infrastruktury po ratelimiting kluczy API zewnętrznych aplikacji) jednym z wyzwań, przed jakimi stoję, jest zabezpieczenie czegoś, co można by nazwać intranetem. Niekoniecznie VPNem, bo nie zewsząd da się do takowego podłączyć, a zwykle i tak chodzi o zestaw webaplikacji - nierzadko napisanych na kolanie bez cienia autoryzacji.
Pierwsze podejście z Caddy’m i ucieczka od niego #
Moje pierwsze podejście do tego tematu zaczęło się ze zgłębianiem dostępnych plug-inów do web serwera Caddy - wówczas w wersji pierwszej. Natknąłem się na sprytny plugin http.login, który za pomocą innego pluginu - jwt umożliwiał integrację z dostawcami tożsamości takimi jak Google, czy GitHub. Wystarczyło utworzyć w panelu własną aplikację OAuth, przekopiować tokeny do konfiguracji plug-inu i wylistować użytkowników mogących się zalogować do zasobów intranetu. Jak to rozwiązanie wygląda w praktyce, można zobaczyć na innym blogu - https://etherarp.net/github-login-on-caddy/index.html
Umożliwiał, bowiem twórcy Caddy’ego postanowili wydać wersję drugą, całkowicie niszcząc system plug-inów. Stara dokumentacja nie jest dostępna - na Web Archive można zobaczyć jak prosto wyglądała konfiguracja - całkowicie zgodna z duchem tego ekosystemu. Wiele miesięcy od otworzenia issue dotyczącego przyszłości plug-inów autoryzacyjnych twórcy dalej nie mają planów na oddanie użytkownikom dość istotnej funkcjonalności.
Dodatkowo od jakiegoś czasu dostawałem maile od Githuba, zatytułowanych [GitHubAPI] Deprecation notice for authentication via URL query parameters
, a prowadzących do
https://developer.github.com/changes/2020-02-10-deprecating-auth-through-query-param/. Przy okazji planowanej jeszcze wówczas migracji do nowej wersji Caddy’ego (nie spodziewając się takich problemów z kompatybilnością) miałem zamiar poprawić ów plugin, żeby GitHub nie narzekał, a sam kod nie przestał działać.
Dlatego też postanowiłem poszukać alternatyw, nawet jeśli miały uwzględniać używanie Nginxa, którego porzuciłem w mojej domowej sieci z wielu względów - między innymi przewagi Caddy’ego na polu współpracy z Let’s Encryptem i pogmatwanych konfigów w małych i niezbyt wymagających projektach.
Drugie podejście - Authelia + nginx #
Szukając alternatyw uznałem, że priorytetem będzie dwuskładnikowe uwierzytelnianie, najlepiej w formie powiadomień push - tak żeby działało to wygodnie na telefonie - cały czas, nie tylko w momencie posiadania pod ręką Yubikeya NFC. Authelia poza klasycznymi TOTP supportuje też Duo, które jest darmowe dla 10 użytkowników w organizacji.
Jednym z pierwszych wyników, a na pewno najbardziej obiecującym rozwiązaniem okazała się Authelia - middleware autoryzacji dla nginxa, traefika i haproxy.
Twórcy dostarczają gotowe setupy dla dockera i integrację z ingress proxy kubernetesa, lecz mój lab oparty na trwałych kontenerach LXC wymagał setupu jak dla baremerala - co okazało się nieco trudniejsze w mmomencie kiedy chciałem za pierwszym razem wyprodukować coś, co przejmie ruch z istniejącego rozwiązania, ale czego się nie robi siedząc do 3 rano 😉
Instalacja i wymagania wstępne #
Na serwer obsługujący ruch HTTP wybrałem znanego sobie nginxa. W tym setupie terminuje także TLSa z certyfikatem z prywatnego CA obsługiwanego przez smallstepa, o którym powinienem kiedyś więcej napisać.
Do kompletu potrzeba będzie także Redisa do przechowywania tokenów sesyjnych - przydaje się to bardziej przy skalowani Authelii, ale pomaga także rozdzielić storage od samego proxy.
Sama instalacja jest dość prosta - nie ma jeszcze co prawda paczek, ale poniższy playbook ansibla rozwiązuje sprawę. Zmienna authelia_ver
ma wartość taga z githuba (na przykład v4.21.0) -
https://github.com/authelia/authelia/releases
- name: install software
apt:
name:
- nginx
- redis
- name: protect redis
lineinfile:
path: /etc/redis/redis.conf
line: requirepass {{redis_pass}}
- name: prepare authelia html dir
file:
path: /srv/authelia
state: directory
owner: www-data
- name: fetch authelia binary
unarchive:
src: https://github.com/authelia/authelia/releases/download/{{authelia_ver}}/authelia-linux-amd64.tar.gz
remote_src: true
dest: /srv/authelia
- name: deploy systemd service
template:
src: authelia/authelia.service.j2
dest: /lib/systemd/system/authelia.service
- name: enable and restart systemd services
systemd:
name: '{{item}}'
state: restarted
enabled: true
with_items:
- redis
- nginx
- authelia
Użyty template serwisu systemd wygląda następująco:
[Unit]
Description=Authelia authentication and authorization server
After=network.target
[Service]
ExecStart=/srv/authelia/authelia-linux-amd64 --config /srv/authelia/configuration.yml
SyslogIdentifier=authelia
[Install]
WantedBy=multi-user.target
Konfigurowanie nginxa #
Czas skonfigurować nginxa tak, żeby coś prostego nam proxował, a Authelia broniła dostępu. Będą nam potrzebne następujące pliki:
/etc/nginx/sites-enabled/default
usunięty/etc/nginx/authelia.conf
definiujący endoint/authelia
do obsługi autoryzacji:
location /authelia {
internal;
set $upstream_authelia http://127.0.0.1:9091/api/verify;
proxy_pass_request_body off;
proxy_pass $upstream_authelia;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header Content-Length "";
# Timeout if the real server is dead
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
# Basic Proxy Config
client_body_buffer_size 128k;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_set_header X-Forwarded-Ssl on;
proxy_redirect http:// $scheme://;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache_bypass $cookie_session;
proxy_no_cache $cookie_session;
proxy_buffers 4 32k;
# Advanced Proxy Config
send_timeout 5m;
proxy_read_timeout 240;
proxy_send_timeout 240;
proxy_connect_timeout 240;
}
/etc/nginx/auth.conf
konfigurujący użycie middleware’u autoryzacji i odpowiedni redirect do strony logowania (którego konfig opisany jest nieco dalej - tutaj jest to domenaauth.example.com
):
auth_request /authelia;
auth_request_set $target_url $scheme://$http_host$request_uri;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
error_page 401 =302 https://auth.example.com/?rd=$target_url;
/etc/nginx/ssl.conf
opisujący gdzie szukać certyfikatów SSL - bardziej przydatne kiedy używamy certyfikatu wildcard; oczywiście potrzeba także/etc/nginx/intranet.*
listen 443 ssl;
ssl_certificate /etc/nginx/intranet.crt;
ssl_certificate_key /etc/nginx/intranet.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
/etc/nginx/sites-enabled/proxy.conf
zawierający konfigurację przezroczystego proxy - to jest coś, co w Caddym można by zapisać jako,proxy / { transparent }
, jednak nginx jest bardziej jak Debian w tej kwestii 😉
client_body_buffer_size 128k;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
send_timeout 5m;
proxy_read_timeout 360;
proxy_send_timeout 360;
proxy_connect_timeout 360;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_set_header X-Forwarded-Ssl on;
proxy_redirect http:// $scheme://;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache_bypass $cookie_session;
proxy_no_cache $cookie_session;
proxy_buffers 64 256k;
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.0.0.0/8;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from fc00::/7;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
/etc/nginx/sites-enabled/auth_portal.conf
definiujący domenę odpowiedzialną za panel logowania;http://127.0.0.1:9091
to standardowy endpoint Authelii
server {
listen 80;
server_name auth.example.com;
location / {
return 301 https://$server_name$request_uri;
}
}
server {
server_name auth.example.com;
include /etc/nginx/ssl.conf;
location / {
add_header X-Forwarded-Host auth.example.com;
add_header X-Forwarded-Proto $scheme;
set $upstream_authelia http://127.0.0.1:9091;
proxy_pass $upstream_authelia;
include proxy.conf;
}
}
- odpowiednie
/etc/nginx/sites-enabled/domena.conf
wyglądające na przykład tak (oczywiściehttp://{{ips.grafana}}:{{ports.grafana}}
to jinjowa templatka wykorzystująca ansiblowe zmienne)
server {
server_name grafana.example.com;
listen 80;
return 301 https://$server_name$request_uri;
}
server {
server_name grafana.example.com;
include ssl.conf;
include authelia.conf;
location / {
set $upstream_target http://{{ips.grafana}}:{{ports.grafana}};
proxy_pass $upstream_target;
include auth.conf;
include proxy.conf;
}
}
Konfigurowanie authelii #
Sama authelia wymaga już tylko jednego pliku konfiguracyjnego oraz bazy użytkowników - może być to plik YAML (który wykorzystamy w tym prostym przykładzie), LDAP lub klasyczna baza danych - SQLite, MySQL tudzież PostgreSQL.
Tu powinna pojawić się także konfiguracja serwera SMTP, ale twórcy Authelii przewidzieli naprawdę małe setupy i jest możliwość zapisywania wiadomości zawierających np. linki do aktywacji MFA czy resetowania hasła w pliku tekstowym na serwerze - idealne dla jednego użytkownika lub celów testowych.
Ścieżka do pliku podana jest w commandline, w tym przykładzie zdefiniowana jest w konfiguracji usługi w systemd (/srv/authelia/configuration.yml
).
host: 127.0.0.1
port: 9091
server:
read_buffer_size: 4096
write_buffer_size: 4096
path: ""
log_level: debug
jwt_secret: {{LONG_RANDOM_STRING}}
default_redirection_url: https://example.com
duo_api:
hostname: {{DUO_HOSTNAME}}
integration_key: {{DUO_INTEGRATION_KEY}}
secret_key: {{DUO_SECRET}}
authentication_backend:
disable_reset_password: false
refresh_interval: 5m
file:
path: /srv/authelia/users_database.yml
password:
algorithm: sha512
iterations: 1
key_length: 32
salt_length: 16
memory: 512
parallelism: 8
access_control:
default_policy: two_factor
rules:
- domain: example.com
policy: bypass
- domain: "*.example.com"
policy: two_factor
session:
name: authelia_session
secret: insecure_session_secret
expiration: 1h
inactivity: 5m
remember_me_duration: 1M
domain: example.com
redis:
host: 127.0.0.1
port: 6379
password: {{redis_pass}}
database_index: 0
regulation:
max_retries: 3
find_time: 2m
ban_time: 5m
storage:
local:
path: /srv/authelia/db.sqlite3
notifier:
disable_startup_check: false
filesystem:
filename: /tmp/notification.txt
Zmienne związane z Duo opiszę w następnej części. Poza tym podmienić należy oczywiście losowy sekret JWT, domenę, hasło do Redisa i zdefiniować odpowiednie reguły chronienia domen. Przykładowe zapewniają dostęp bez logowania do domeny głównej i chroniony MFA dla wszelkich subdomen. Więcej opcji opisanych jest w bardzo dobrej dokumentacji - https://www.authelia.com/docs/configuration/access-control.html
Ostatnim klockiem w układance jest plik /srv/authelia/users_database.yml
. O tym jak wygenerować hash hasła wspomina dokumentacja -
https://www.authelia.com/docs/configuration/authentication/file.html#passwords
Coś, co jest warte uwagi przy deploymencie na lekkich kontenerach (mój ma 128 MB RAMu i 1 vCPU) to fakt, że domyślnie używany algorytm hashujący argon2id jest wybitnie ciężki - użyłem zamiast niego sha512.
users:
daniel:
displayname: "Daniel Skowroński"
password: "{{HASHED}}"
email: [email protected]
groups:
- admins
Konfigurowanie Duo #
Na koniec konfiguracji potrzebujemy ustawionego Duo. Wystarczy konto Duo Free, które na stronie opisane jest jako trial, ale nim nie jest - jest darmowe ( https://duo.com/pricing/duo-free). W procesie rejestracji potrzebujemy aplikacji Duo na telefonie, bowiem Admin Login chroniony jest przez Duo 😉
Po zalogowaniu się w domenie admin.duosecurity.com należy wybrać Protect new application i odnaleźć pozycję Partner Auth API. Powstanie nowa aplikacja, którą możemy przemianować scrollując jej stronę niżej do Settings. To, co na pewno trzeba zrobić to zapisać w konfiguracji Authelii wartości integration key, secret key oraz domain.
Logowanie do systemu #
Po zrestartowaniu nginxa oraz Authelii czas na logowanie.
Enrollowanie użytkownika do Duo #
Proces enrolowania wykonujemy całkowicie po stronie panelu administracyjnego Duo. Aby dodać użytkownika należy wybrać Users -> Add user. Trzeba pamiętać, żeby dodać mu odpowiednie aliasy i adres e-mailowy pasujące do tych z bazy danych Authelii. Nie można wykorzystać użytkownika panelu administracyjnego Duo, ale nic nie stoi na przeszkodzie, by używać tego samego maila czy loginu.
Kolejny krok to dodanie urządzenia autoryzującego, w naszym wypadku telefonu z aplikacją Duo do obsługi powiadomień push. Po przewinięciu strony użytkownika do dołu znajdziemy link Add phone
Następnie wybieramy typ urządzenia. Phone jest przydatne przy enrollmencie po numerze telefonu - kod przychodzi SMSem, Tablet to wybór dla urządzeń bez numeru telefonu - wiadomość przyjdzie mailem.
Teraz należy aktywować urządzenie poprzez wysłanie maila z linkiem i kodem.<
Mail przychodzi od Duo - co jest wygodniejsze niż opisana za chwilę opcja z TOTP od Authelii. Instrukcje dla użytkownika są dość proste.
Enrollowanie użytkownika do klasycznego TOTP #
Zawsze można używać klasycznego TOTP jako backupu - za pomocą dowolnej aplikacji typu Authy czy Google Authenticator. Tutaj procedura jest nieco bardziej zawiła i wymaga użycia wspomnianego pliku z powiadomieniami lub setupu SMTP. Pierwszym krokiem jest wybranie po zalogowaniu do Authelii Methods -> One-Time Password -> Not registered yet. Następnie należy przegrepować plik z powiadomieniami, wybrać z niego link do rejestracji, otworzyć go i zeskanować dowolną aplikacją do TOTP kod QR.
Podsumowanie #
Niewielkim nakładem konfiguracji można dodać Authelię do istniejącego intranetu - wystarczy nginx wystawiony na świat by obsługiwał HTTP/HTTPS, middleware Authelii decyduje czy dana domena ma być dostępna dla wszystkich, czy nie, a jeśli potrzeba uwierzytelnia użytkowników - łącząc się z bazą danych, LDAPem lub prostym plikiem YAML, a całości dopełnia darmowe konto Duo i powiadomienia push. Ponadto w razie potrzeby całość łatwo się skaluje.
A decyzje twórców Caddiego łatwo się szkaluje - okazało się to kolejne oprogramowanie opensource (swoją drogą z dostępną wersją z supportem, ale z irytującą praktyką doklejania headerów http dla wersji free, czyli oficjalnych buildów i zakazem jej używania do celów komercyjnych - póki samodzielnie się go nie zbuduje), które kusząc bardzo atrakcyjnymi ułatwiaczami w rodzaju implementacji inicjatywy HTTPS Everywhere, supportowi HTTP/2 od wczesnych dni, czy banalnym plikiem konfiguracyjnym jednocześnie robione jest na szybko i bez przemyślenia - co wyszło zwłaszcza przy aktualizacji do v2, która zniszczyła system plug-inów, nie zapewniając kompatybilności wstecznej ani nawet przeniesienia API dla wielu middlewerów tak, że nie da się ich nawet portować na nową wersję. Ba, usunięto też możliwość pobrania wersji binarki z wybranymi plug-inami - ponieważ Caddy napisany jest w Go, jest skompilowany statycznie i plug-iny są częścią pliku wykonywalnego. Do tej pory można było pobrać plik z URLa w formie ?plugins=jwt,auth,...
, teraz trzeba kompilować całość samodzielnie lub wybrać wersję bez supportu dla innych plug-inów.