Skip to main content
  1. Posts/
  2. blog.dsinf.net/

Ochrona własnego intranetu za pomocą rozwiązania Authelia i historia odejścia od web serwera Caddy

·1875 words·9 mins
blog.dsinf.net caddy https nginx proxy security
Daniel Skowroński
Author
Daniel Skowroński
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 domena auth.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ście http://{{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.