개요
여러가지 도커 이미지를 이용해 셀프 호스팅으로 서비스를 올려보고 사용해보는 재미에 빠지다 보면 만나는 문제가 한 가지 있습니다. 바로 서비스에 접속할 때마다 일일히 로그인을 해야 한다는 점이죠.
Immich, Plex, Syncthing, Komga, Sonarr, Radarr 기타 등등의 서비스를 접속할때마다 로그인을 해야 한다고 하면, 조금 지치는 게 사실입니다.
거기다, 주변 지인들에게 각각의 서비스를 이용할 수 있게 계정을 만들어서 주려고 할 때, 각각의 앱마다 일일히 계정을 생성해서 건네주어야 할 겁니다. 이용하는 사람도 각각의 서비스마다 매번 로그인을 해가면서 이용해야 하고요.
이럴 때 필요한 것이 Identity and Access Management (줄여서 IAM) 솔루션입니다.
Authentik은 IAM솔루션의 한 종류로, LDAP와 같은 디렉토리 서비스를 통합하여 사용자 정보를 관리하고,
OAuth2, OIDC(OpenID Connect) 등의 인증 프로토콜을 통해 다양한 서비스와의 손쉬운 통합을 지원하며,
SSO(Single Sign-On) 기능을 통해 여러 서비스(어플리케이션)에 대해 단일 로그인 자격 증명을 사용해 접근할 수 있게 해 줍니다.
처음이 어렵지, 구축해놓으면 굉장히 편리한 기능 중 하나이며, 도커 설치를 지원하기 때문에, 접근성이 좋은 것도 장점입니다.
작성일 현재 TrueNAS공식 버전은 없고, Truecharts 버전은 설치 중 에러가 발생해, 우분투에서 도커로 설치한 뒤, 예시를 위해 Nextcloud를 LDAP로 연결해보겠습니다.
Authentik 설치하기
공식 docker-compose 다운받기
공식 설치 가이드는 docker compose구문을 아래와 같이 미리 제공하고 있습니다.
wget https://goauthentik.io/docker-compose.yml
---
version: "3.4"
services:
postgresql:
image: docker.io/library/postgres:12-alpine
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
start_period: 20s
interval: 30s
retries: 5
timeout: 5s
volumes:
- database:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
POSTGRES_DB: ${PG_DB:-authentik}
env_file:
- .env
redis:
image: docker.io/library/redis:alpine
command: --save 60 1 --loglevel warning
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
restart: unless-stopped
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
volumes:
- ./media:/media
- ./custom-templates:/templates
env_file:
- .env
ports:
- "${COMPOSE_PORT_HTTP:-9000}:9000"
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
depends_on:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
restart: unless-stopped
command: worker
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
# `user: root` and the docker socket volume are optional.
# See more for the docker socket integration here:
# https://goauthentik.io/docs/outposts/integrations/docker
# Removing `user: root` also prevents the worker from fixing the permissions
# on the mounted folders, so when removing this make sure the folders have the correct UID/GID
# (1000:1000 by default)
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
env_file:
- .env
depends_on:
- postgresql
- redis
volumes:
database:
driver: local
redis:
driver: local
마운트 경로 준비하기
위에서 다운받은 docker-compose 구문을 보면, 두 개의 볼륨(database, redis)마운트와 세 개의 바인드마운트(./media, ./certs, ./custom-templates)로 구성되어 있습니다.
저는 바인드마운트를 선호하기 때문에, 볼륨마운트로 구성된 부분도 바인드마운트로 수정해서 사용할 예정이라, 미리 아래와 같이 경로를 만들어 두겠습니다.
root@gate:/data/authentik# mkdir certs custom-templates media redis database
root@gate:/data/authentik# ls -lah
drwxr-xr-x 1 root root 78 5월 16 16:46 .
drwxr-xr-x 1 root root 56 5월 16 16:35 ..
drwxr-xr-x 1 root root 0 5월 16 16:46 certs
drwxr-xr-x 1 root root 0 5월 16 16:46 custom-templates
drwxr-xr-x 1 root root 0 5월 16 16:46 database
drwxr-xr-x 1 root root 0 5월 16 16:46 media
drwxr-xr-x 1 root root 0 5월 16 16:46 redis
/data/authentik 아래에 총 5개의 폴더가 잘 만들어졌음을 확인할 수 있습니다.
환경 변수 입력하기
공식 설치 가이드는 docker-compose와 env파일을 이용합니다. 우선 아래 명령어를 쉘에 입력하여 비밀번호와 키를 생성합니다. openssl은 왠만하면 기본 패키지로 설치되어 있으므로 잘 작동하겠지만(심지어 시놀로지에도 설치되어 있음), 혹시 설치되어 있지 않다면 apt install로 설치해 주시면 됩니다.
echo "PG_PASS=$(openssl rand 36 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 60 | base64)" >> .env
편집기를 열어 확인해보면, 랜덤한 문자열로 잘 생성된 것을 확인할 수 있습니다.
nano .env
이제 여기다가 이메일 발송을 위한 환경변수를 입력하겠습니다. 공식 홈페이지에는 아래와 같이 예시가 적혀 있습니다.
# SMTP Host Emails are sent to
AUTHENTIK_EMAIL__HOST=localhost
AUTHENTIK_EMAIL__PORT=25
# Optionally authenticate (don't add quotation marks to your password)
AUTHENTIK_EMAIL__USERNAME=
AUTHENTIK_EMAIL__PASSWORD=
# Use StartTLS
AUTHENTIK_EMAIL__USE_TLS=false
# Use SSL
AUTHENTIK_EMAIL__USE_SSL=false
AUTHENTIK_EMAIL__TIMEOUT=10
# Email address authentik will send from, should have a correct @domain
AUTHENTIK_EMAIL__FROM=authentik@localhost
다음메일을 사용한다고 가정했을 때(username = example, password=sample), 위에서부터 각각 기입하면 이렇게 됩니다.
그리고 Authentik은 HTTP로 9000, HTTPS로 9443번 포트를 기본으로 점유하는데, 이를 변경하고 싶다면 아래와 같이 입력하면 됩니다.
예시는 1000, 1443번으로 변경합니다.
COMPOSE_PORT_HTTP=1000
COMPOSE_PORT_HTTPS=1443
여기까지 다 입력하면 .env의 내용이 아래처럼 되겠네요.
저장 후 닫아줍시다.
Portainer로 Authentik 실행하기
저는 portainer로 컨테이너를 띄울 예정입니다. 기본적으로 docker-compose와 동일하나,
- version 부분을 제외해야 하고
- portainer는 env파일로 환경변수를 입력할 때 “stack.env”로 파일명을 입력해 주어야 하며
- 공식 compose와 달리 볼륨 마운트를 바인드 마운트로 변경할 예정이고
- 공식 compose는 docker-compose.yml의 위치에 따른 상대 경로를 사용해서 ./media와 같이 표기했지만 portainer를 쓰는 저는 절대 경로를 입력해 주어야 합니다.
따라서 최종적으로 docker-compose는 아래와 같이 됩니다.
services:
postgresql:
image: docker.io/library/postgres:12-alpine
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
start_period: 20s
interval: 30s
retries: 5
timeout: 5s
volumes:
- /data/authentik/database:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
POSTGRES_DB: ${PG_DB:-authentik}
env_file:
- stack.env
redis:
image: docker.io/library/redis:alpine
command: --save 60 1 --loglevel warning
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- /data/authentik/redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
restart: unless-stopped
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
volumes:
- /data/authentik/media:/media
- /data/authentik/custom-templates:/templates
env_file:
- stack.env
ports:
- "${COMPOSE_PORT_HTTP:-9000}:9000"
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
depends_on:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
restart: unless-stopped
command: worker
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
# `user: root` and the docker socket volume are optional.
# See more for the docker socket integration here:
# https://goauthentik.io/docs/outposts/integrations/docker
# Removing `user: root` also prevents the worker from fixing the permissions
# on the mounted folders, so when removing this make sure the folders have the correct UID/GID
# (1000:1000 by default)
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /data/authentik/media:/media
- /data/authentik/certs:/certs
- /data/authentik/custom-templates:/templates
env_file:
- stack.env
depends_on:
- postgresql
- redis
그리고 env파일의 내용은 .env를 직접 업로드해도 되고, 붙여넣어도 됩니다.
Advanced mode를 클릭하면, 파일 내용 그대로 붙여넣을 수 있고 이후 Simple Mode로 변경하면 environment값이 하나씩 제대로 들어가는 것을 확인할 수 있습니다.
이제 Deploy the stack을 클릭해 컨테이너를 실행하면 됩니다. 총 4개의 컨테이너가 deploy됩니다(authentik-server, authentik-worker, redis, postgresql).
Authentik 초기 설정
관리자 계정 생성하기
http://<IP:PORT>/if/flow/initial-setup/로 접속하면 초기설정을 시작할 수 있습니다. 개인 환경에 맞게 admin계정의 E-mail과 비밀번호를 입력해 줍니다.
간단한 초기 설정 단계를 지나면 아래처럼 authentik 내부에 접속하게 됩니다.
초기 관리자 계정은 akadmin입니다.
우측 상단의 관리자 인터페이스를 클릭하면 아래처럼 관리자 메뉴로 진입할 수 있습니다.
기본 계정을 변경하려면 우측 상단의 관리자 인터페이스 – 디렉토리 – 사용자 – akadmin에서 변경할 수 있습니다.
인증서 적용하기
아래 3가지 방법을 기술해 보겠습니다.
- 자체서명 인증서 사용
- 리버스 프록시 서비스(Nginx Proxy Manager)에서 가져오기
- certbot으로 인증서 발행하기(추천)
자체서명 인증서 사용하기
Authentik에는 기본적으로 자체서명된 인증서가 있습니다. OAuth/OpenID Connect방식은 서버측과 클라이언트 측에 모두 인증서 구성을 요구하지 않으므로 이를 이용해서도 충분히 구현할 수 있으므로 별도의 설정 없이 미리 준비된 인증서를 사용하면 됩니다.
자체서명 인증서를 사용하기로 결정한 경우 이 단락은 미련 없이 건너뛰셔도 됩니다.
Nginx Proxy Manager로부터 가져오기
사용중인 와일드카드 인증서가 있으니 해당 인증서를 가져오는 과정을 진행해 보겠습니다.
제 와일드카드 인증서는 로컬에서 작동하고 있는 Nginx Proxy Manager(이하 “NPM”)가 책임지고 있고, Cloudflare DNS Challenge방식으로 발급되고 있습니다. Let’s Encrypt구요.
해당 컨테이너에 바인드마운트한 경로(/data/npm/letsencrypt)에서 인증서가 저장된 경로를 정확히 찾아보니 아래와 같았습니다.
(여러개의 인증서를 사용한다면 경로가 조금 더 복잡할 것으로 예상됩니다.)
/data/npm/letsencrypt/archive/npm-1
그리고 해당 경로의 인증서는 아래와 같이 생성되어 있었습니다.
:/data/npm/letsencrypt/archive/npm-1# ls -lah
drwxr-xr-x 1 root root 450 5월 2 19:58 .
drwx------ 1 root root 10 9월 5 2023 ..
-rw-r--r-- 1 root root 1.5K 9월 5 2023 cert1.pem
-rw-r--r-- 1 root root 1.5K 11월 4 2023 cert2.pem
-rw-r--r-- 1 root root 1.5K 1월 3 20:57 cert3.pem
-rw-r--r-- 1 root root 1.5K 3월 3 20:28 cert4.pem
-rw-r--r-- 1 root root 1.5K 5월 2 19:58 cert5.pem
-rw-r--r-- 1 root root 1.8K 9월 5 2023 chain1.pem
-rw-r--r-- 1 root root 1.8K 11월 4 2023 chain2.pem
-rw-r--r-- 1 root root 1.8K 1월 3 20:57 chain3.pem
-rw-r--r-- 1 root root 1.8K 3월 3 20:28 chain4.pem
-rw-r--r-- 1 root root 1.8K 5월 2 19:58 chain5.pem
-rw-r--r-- 1 root root 3.3K 9월 5 2023 fullchain1.pem
-rw-r--r-- 1 root root 3.3K 11월 4 2023 fullchain2.pem
-rw-r--r-- 1 root root 3.3K 1월 3 20:57 fullchain3.pem
-rw-r--r-- 1 root root 3.3K 3월 3 20:28 fullchain4.pem
-rw-r--r-- 1 root root 3.3K 5월 2 19:58 fullchain5.pem
-rw------- 1 root root 306 9월 5 2023 privkey1.pem
-rw------- 1 root root 306 11월 4 2023 privkey2.pem
-rw------- 1 root root 306 1월 3 20:57 privkey3.pem
-rw------- 1 root root 306 3월 3 20:28 privkey4.pem
-rw------- 1 root root 306 5월 2 19:58 privkey5.pem
Let’s Encrypt로 생성된 인증서가 3개월의 유효기간을 가지고 있고, 만기일 1달 전부터 갱신 가능해진다는 점을 생각했을 때,
NPM은 인증서의 갱신에 성공할때마다 넘버링을 +1씩 해가며 파일을 저장하고 있음을 알 수 있고, 동시에 인증서를 이루는 파일은 아래와 같이 총 4개라는 사실도 알 수 있습니다.
- cert.pem (서버용 인증서)
- chain.pem (중간 인증서)
- fullchain.pem (서버용 + 중간 인증서)
- privkey.pem (개인 키)
이 파일들을 authentik에 전달하기 위해 docker-compose내에 구성했던 certs폴더를 활용하겠습니다.
이 때, cert + chain = fullchain이므로 4개를 다 import할 필요는 없고 2개(fullchain.pem, privkey.pem)만 가져가면 됩니다.
이 글대로 컨테이너를 띄웠다면, 컨테이너 내 certs의 로컬 경로는 /data/authentik/certs입니다.
cd /data/npm/letsencrypt/archive/npm-1
cp -p fullchain5.pem privkey5.pem /data/authentik/certs
ls -lah /data/authentik/certs
아래와 같이 인증서가 제대로 복사되었음을 확인할 수 있습니다.
(이하 스크린샷들은 테스트 겸 4개를 모두 복사한 스크린샷입니다. 2개만 복사하셔도 됩니다.)
이제 authentik 컨테이너 내부에서 명령어를 실행해야 합니다. 이 작업을 수행할 컨테이너는 authentik-worker컨테이너이므로, docker ps -a로 worker컨테이너의 이름을 확인해봐야 합니다.
저처럼 portainer를 사용중이라면 해당 컨테이너를 찾아서 바로 쉘로 들어가도 됩니다.
authentik-worker-1임을 알 수 있습니다. 아래 명령어를 통해서 컨테이너 내부쉘로 진입해 certs 폴더 내부를 확인해 보겠습니다.
docker exec -it authentik-worker-1 /bin/bash
ls -lah /certs
바인드마운트이기 때문에, 컨테이너가 바로 인증서 파일들을 인식하고 있습니다. 이제 아래 명령어를 입력해서 import과정을 거쳐야 합니다.
ak import_certificate --private-key /certs/privkey5.pem --certificate /certs/fullchain5.pem --name letsencrypt
그러면 아래와 같은 log가 출력됩니다.
{"event": "Loaded config", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922800.0837514, "file": "/authentik/lib/default.yml"}
{"event": "Loaded environment variables", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922800.084518, "count": 14}
{"event": "Starting authentik bootstrap", "level": "info", "logger": "authentik.lib.config", "timestamp": 1715922800.5446625}
{"event": "PostgreSQL connection successful", "level": "info", "logger": "authentik.lib.config", "timestamp": 1715922800.558939}
{"event": "Redis Connection successful", "level": "info", "logger": "authentik.lib.config", "timestamp": 1715922800.5607312}
{"event": "Finished authentik bootstrap", "level": "info", "logger": "authentik.lib.config", "timestamp": 1715922800.5609217}
{"event": "Booting authentik", "level": "info", "logger": "authentik.lib.config", "timestamp": 1715922802.8878634, "version": "2024.4.2"}
{"event": "Enabled authentik enterprise", "level": "info", "logger": "authentik.lib.config", "timestamp": 1715922802.8930213}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.8945675, "path": "authentik.enterprise.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.8959653, "path": "authentik.policies.reputation.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.9013422, "path": "authentik.blueprints.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.903196, "path": "authentik.enterprise.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.9059677, "path": "authentik.sources.plex.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.9078548, "path": "authentik.crypto.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.9099498, "path": "authentik.sources.ldap.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.9148462, "path": "authentik.stages.authenticator_totp.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.9158354, "path": "authentik.providers.scim.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.9167688, "path": "authentik.admin.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.9192605, "path": "authentik.outposts.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.9202347, "path": "authentik.sources.oauth.settings"}
{"event": "Loaded app settings", "level": "debug", "logger": "authentik.lib.config", "timestamp": 1715922802.921979, "path": "authentik.events.settings"}
/ak-root/venv/lib/python3.12/site-packages/opencontainers/distribution/reggie/defaults.py:17: SyntaxWarning: invalid escape sequence '\('
"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
{"domain_url": null, "event": "Loaded MMDB database", "file": "/geoip/GeoLite2-ASN.mmdb", "last_write": 1715093837.0, "level": "info", "logger": "authentik.events.context_processors.mmdb", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:24.364019"}
{"domain_url": null, "event": "Loaded MMDB database", "file": "/geoip/GeoLite2-City.mmdb", "last_write": 1715093836.0, "level": "info", "logger": "authentik.events.context_processors.mmdb", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:24.365862"}
{"app_name": "authentik.tenants", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.tenants.checks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.724414"}
{"app_name": "authentik.tenants", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.tenants.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.725328"}
{"app_name": "authentik.admin", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.admin.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.816738"}
{"app_name": "authentik.admin", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.admin.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.817735"}
{"app_name": "authentik.crypto", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.crypto.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.819552"}
{"app_name": "authentik.flows", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.flows.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.881939"}
{"app_name": "authentik.outposts", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.outposts.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.937135"}
{"app_name": "authentik.outposts", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.outposts.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.938707"}
{"app_name": "authentik.policies.reputation", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.policies.reputation.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.940363"}
{"app_name": "authentik.policies.reputation", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.policies.reputation.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.942392"}
{"app_name": "authentik.policies", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.policies.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.959635"}
{"app_name": "authentik.providers.proxy", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.providers.proxy.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.961018"}
{"app_name": "authentik.providers.proxy", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.providers.proxy.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:26.962056"}
{"app_name": "authentik.providers.scim", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.providers.scim.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.376093"}
{"app_name": "authentik.providers.scim", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.providers.scim.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.377625"}
{"app_name": "authentik.rbac", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.rbac.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.379832"}
{"app_name": "authentik.sources.ldap", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.sources.ldap.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.393671"}
{"app_name": "authentik.sources.ldap", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.sources.ldap.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.397390"}
/ak-root/venv/lib/python3.12/site-packages/facebook/__init__.py:99: SyntaxWarning: invalid escape sequence '\d'
version_regex = re.compile("^\d\.\d{1,2}$")
{"app_name": "authentik.sources.oauth", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.sources.oauth.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.416370"}
{"app_name": "authentik.sources.saml", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.sources.saml.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.417421"}
{"app_name": "authentik.sources.scim", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.sources.scim.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.418682"}
{"app_name": "authentik.stages.authenticator_duo", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.stages.authenticator_duo.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.419940"}
{"app_name": "authentik.stages.authenticator_static", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.stages.authenticator_static.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.421101"}
{"app_name": "authentik.stages.authenticator_webauthn", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.stages.authenticator_webauthn.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.493951"}
{"app_name": "authentik.stages.email", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.stages.email.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.494499"}
{"app_name": "authentik.core", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.core.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.499165"}
{"app_name": "authentik.core", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.core.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.499446"}
{"app_name": "authentik.enterprise", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.enterprise.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.500697"}
{"app_name": "authentik.enterprise", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.enterprise.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.501918"}
{"app_name": "authentik.enterprise.providers.rac", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.enterprise.providers.rac.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.511010"}
{"app_name": "authentik.events", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.events.tasks", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.511735"}
{"app_name": "authentik.events", "domain_url": null, "event": "Imported related module", "level": "info", "logger": "authentik.blueprints.apps", "module": "authentik.events.signals", "pid": 5454, "schema_name": "public", "timestamp": "2024-05-17T05:13:27.511941"}
Switching to schema 'public'
{"domain_url": null, "event": "Task published", "level": "info", "logger": "authentik.root.celery", "pid": 5454, "schema_name": "public", "task_id": "87f966e9a7ea4347be4e89ac54237b51", "task_name": "authentik.outposts.tasks.outpost_post_save", "timestamp": "2024-05-17T05:13:28.537702"}
관리자 인터페이스에서 시스템 – 인증서 탭을 확인하면 아래처럼 인증서가 제대로 등록된 것을 확인할 수 있습니다.
이후, 아래에서 서술할 ‘공급자 설정’단계에서 이 인증서를 사용할 수 있습니다.
그런데, 3개월마다 갱신기간이 도래하는 인증서를 그 때 그 때 갱신하여 복사해 넣는 과정은 좀 번거롭죠.
(메인 프로덕션 환경의 웹 사이트들도 가끔 인증서 갱신을 잊어버려 서비스 먹통이 되는 경우가 있는데..무려 1년짜린데..=ㅅ=)
그러므로, 스크립트를 짜서 crontab에 등록해서 NPM 컨테이너가 인증서를 갱신시켜주면 authentik에 보내서 import하도록 스크립트를 짜보겠습니다.
이 때, docker-compose에 컨테이너 이름이 지정되어 있지 않아, CLI에서 컨테이너를 스크립트로 특정하기 위해서 아래 명령어를 사용합니다.
docker ps -a | grep 'authentik-worker' | awk '{print $(NF)}'
이름에 authentik-worker가 포함된 컨테이너의 이름을 출력하는 명령어입니다. 수행하면 아래같이 출력됩니다.
#!/bin/bash
# 변수 설정
cert_dir="/data/npm/letsencrypt/archive/npm-1"
dest_dir="/data/authentik/certs"
container=$(docker ps -a | grep 'authentik-worker' | awk '{print $(NF)}')
# 가장 최근 인증서 파일 찾기
latest_fullchain=$(ls -t $cert_dir/fullchain*.pem | head -n 1)
latest_privkey=$(ls -t $cert_dir/privkey*.pem | head -n 1)
# 도커 내부로 전송
cp -p "$latest_fullchain" "$dest_dir/fullchain.pem" --verbose
cp -p "$latest_privkey" "$dest_dir/privkey.pem" --verbose
echo "인증서 복사가 완료되었습니다."
# authentik-worker에서 인증서 가져오기
docker exec -it $container ak import_certificate --private-key /certs/privkey.pem --certificate /certs/fullchain.pem --name letsencrypt
# 복사한 인증서 삭제
rm -rf $dest_dir/*
echo "인증서 가져오기 작업 완료"
인증서가 저장된 위치에서 시간순(-t)으로 검색해 가장 마지막 파일을 하나씩 찾아서 숫자만 제거하고 복사한 뒤 certificate import 명령을 실행하는 스크립트입니다.
비슷한 방식을 워드프레스 백업하기에서 사용한 적이 있습니다.
2025.01.22 - [Apps] - 오라클 클라우드에 도커로 워드프레스 구축하고 백업하기
오라클 클라우드에 도커로 워드프레스 구축하고 백업하기
개요1년도 더 전에 오라클 프리티어 A1을 만들어두고 구글 드라이브 무제한과 연동한 노하드 Plex 라이브러리를 구축하려 했으나, 구글의 통수를 씨게 얻어맞은 후로, 오라클 프리티어를 마땅히
worklazy.net
이제 이 스크립트를 crontab에 등록하는데, 자주 할 필요는 없고 1주일에 한 번 정도면 됩니다.
/data/authentik/importcerts.sh로 저장한 후, crontab에 등록해 줍니다.
crontab -e
30 23 * * 7 /data/authentik/importcerts.sh >> /data/authentik/importcerts.sh.log 2>&1
이러면 매주 일요일 밤 11시 30분에 한 번씩 스크립트를 실행하고 importcerts.sh.log에 기록을 남기게 됩니다.
공식 문서에서도 이 작업은 인증서에 변경사항이 있을 때만 갱신되기 때문에 cron job으로 수행해도 안전하다고 안내하고 있습니다.
그.러.나. 별도의 설정 없이 NPM 컨테이너를 사용하시는 분들은 ECDSA 암호화 방식의 인증서가 생성되어 있을 겁니다(스크린샷처럼). 이 인증서를 Cloudflare가 받아주질 못합니다.
큰 문제는 아닌데, Cloudflare의 Access에서 인증 수단으로 Authentik을 사용하려 할 경우에는 이 인증서 문제 때문에 클라이언트 정보를 제대로 받지 못합니다.
NPM컨테이너를 생성할 때 environment값에 RSA를 지정하면 RSA기반의 인증서를 발급할 수 있긴 하지만, NPM이 적용되는 다른 사이트 주소에 사용되는 인증서도 전부 RSA가 적용되겠죠.
사용중인 다른 환경의 인증서가 영향받는 걸 꺼리신다면, Cloudflare에서 Access – Applications에서 생성한 policy에 대해 Bypass를 설정하면 사용할 수 있습니다.
그러나 이건 이거대로.. 추천하지 않습니다 =ㅅ=.
certbot으로 인증서 발행하기
certbot은 네이티브로도 사용할 수 있지만, 서비스 구축 자체를 docker로 하고 있으니 certbot도 도커로 사용해 봅시다.
certbot이미지는 인증서를 생성한 뒤엔, 프로세스가 없어 종료되어 버리기 때문에, crontab을 이용해서 주기적으로 실행시키는 방식을 사용할 겁니다.
그러기 위해선 portainer보다는 docker compose가 조금 더 편하니까 이쪽으로 진행해 보겠습니다.
(portainer의 stack도 CLI에서 자동화를 할 수 있는데 조금 더 복잡합니다.)
우선 클플에 로그인해 API를 가져옵니다.
순차적으로 우측 상단 – Appearance – API Tokens – Create Token를 따라갑니다.
Edit zone DNS을 선택하여 아무것도 건드리지 말고 Continue to summary를 클릭합니다.
단, 도메인을 2개 이상 연결하신 분들은 노란색 박스 안에서 어떤 도메인에 대해 설정할 것인지를 선택해야 할 수도 있습니다.
이후 Update token을 클릭하면 아래처럼 랜덤문자열이 출력되는데 잘 복사해 둡니다. 이 화면을 빠져나가면 더 이상 보이지 않습니다 =ㅅ=.
이제 쉘로 돌아와서 cloudflare.ini라는 파일을 만들고 이 안에 토큰을 붙여넣을 겁니다.
nano cloudflare.ini
# Cloudflare API token
dns_cloudflare_api_token = TOKEN
그리고 저장해 줍니다. 저는 이 파일을 authentik의 certs폴더 아래에 두겠습니다.
mv cloudflare.ini /data/authentik/certs/
그리고 docker-compose.yml을 아래처럼 만들겠습니다. 경로는 편하신 곳에 두시면 됩니다. 저는 /data/certbot-cloudflare/docker-compose.yml로 하겠습니다.
version: "3"
services:
certbot:
image: certbot/dns-cloudflare
volumes:
- /data/authentik/certs:/etc/letsencrypt
command: certonly --key-type rsa --force-renewal --non-interactive --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini --agree-tos --server https://acme-v02.api.letsencrypt.org/directory -d -m
맨 마지막 -d에 인증서를 발급할 도메인명(이 글에서는 authsample.fentanest.com), -m에 Cloudflare 이메일(ID)를 입력하면 됩니다.
어차피 생성된 인증서는 곧바로 authentik에서 import해야되기 때문에, authentik의 certs폴더를 바인드마운트 해주었습니다.
이 때, DNS Challenge로 와일드카드 인증서를 발급받으려고 한다면 도메인은 *.domain.com 형식으로 입력하고 command맨 뒤에 아래 내용을 추가해주어야 합니다.
--preferred-challenges dns-01
docker compose up 해주면 아래와 같은 로그가 찍히게 됩니다.
이후 해당 경로를 살펴보면, live폴더와 그 밑에 도메인 명으로 된 폴더가 생성되어 있으며, 그 안에 4개의 파일(cert.pem, chain.pem, fullchain.pem, privkey.pem)이 생성되어있는 것을 확인하실 수 있습니다.
/data/authentik/certs/live/authsample.fentanest.com# ls -lah
drwxr-xr-x 1 root root 94 5월 17 20:40 .
drwx------ 1 root root 60 5월 17 20:40 ..
-rw-r--r-- 1 root root 692 5월 17 20:40 README
lrwxrwxrwx 1 root root 48 5월 17 20:40 cert.pem -> ../../archive/authsample.fentanest.com/cert1.pem
lrwxrwxrwx 1 root root 49 5월 17 20:40 chain.pem -> ../../archive/authsample.fentanest.com/chain1.pem
lrwxrwxrwx 1 root root 53 5월 17 20:40 fullchain.pem -> ../../archive/authsample.fentanest.com/fullchain1.pem
lrwxrwxrwx 1 root root 51 5월 17 20:40 privkey.pem -> ../../archive/authsample.fentanest.com/privkey1.pem
위에서 봤던 방식이죠? 똑같이 authentik에서 import하면 됩니다. 일단 컨테이너에 진입하고,
docker exec -it authentik-worker-1 /bin/bash
컨테이너 루트에서 아래와 같이 입력하겠습니다.
ak import_certificate --private-key /certs/live/authsample.fentanest.com/privkey.pem --certificate /certs/live/authsample.fentanest.com/fullchain.pem --name letsencrypt2
그럼 또 거대한 로그가 지나가게 되고, 관리자 인터페이스를 확인하면 아래와 같이 authsample.fentanest.com에 대해 정상적으로 인증서를 확인할 수 있습니다.
잘 작동되는 것을 확인했다면, 스크립트로 만들어 자동화해두는 것이 편합니다.
/data/certbot-cloudflare/LEcertbot.sh로 작성하겠습니다.
#!/bin/bash
# 변수 설정
path="/data/certbot-cloudflare/docker-compose.yml"
container=$(docker ps -a | grep 'authentik-worker' | awk '{print $(NF)}')
domain="authsample.fentanest.com"
# certbot 컨테이너 올리기
docker compose -f "$path" up
# 인증서 재발급 및 컨테이너 종료 대기
echo "15초 대기중입니다."
sleep 15
# authentik-worker에서 인증서 가져오기
docker exec -it $container ak import_certificate --private-key "/certs/live/$domain/privkey.pem" --certificate "/certs/live/$domain/fullchain.pem" --name letsencrypt
echo "인증서 가져오기 작업 완료"
NPM에서 인증서 가져오기와 비교했을 때, 폴더를 비우는 과정이 빠졌는데, certbot이 인증서를 갱신할 때, 기존 인증서를 요구하기 때문입니다.
README파일에도 이를 지우지 말라고 경고하고 있습니다.
이제 crontab에 등록하면 됩니다.
crontab -e
30 23 * * 7 /data/certbot-cloudflare/LEcertbot.sh >> /data/certbot-cloudflare/LEcertbot.sh.log 2>&1
도메인 연결하기(리버스 프록시)
저는 리버스 프록시를 Cloudflare Tunnel로 구성해 놓은 상태입니다.
위에 NPM얘기하지 않았냐!? 싶으실텐데, 대부분은 Cloudflare로 이용하고 업로드 용량 제한을 우회해야 되는 특정 서비스 몇 가지만 NPM으로 이용하고 있습니다.
사용중인 환경에 맞게 Cloudflare Tunnel에 아래와 같이 CNAME을 구성하겠습니다.
만일 Nginx Proxy Manager 등 다른 리버스 프록시 서비스를 사용하고 있다면, 거기에 구성하시면 됩니다.
문제 해결(Trouble-Shooting)
초기 설정 시 “Not Found”가 나타날 경우
초기 설정 시 접속을 “http://<IP:PORT>/if/flow/initial-setup/”로 하지 않고 “http://<IP:PORT>/if/flow/initial-setup”로 할 경우(맨 끝 슬래시 하나 없을 경우) Not Found를 만나게 됩니다.
마지막 슬래시까지 붙여서 접속하셔야 합니다.
초기 설정 시 “요청이 거부되었습니다.”가 나타날 경우
초기 설정 진행 중 “Flow does not apply to current user”라는 문구가 출력될 경우입니다.
github에도 몇몇 issue로 등록이 되어 있는데, 브라우저를 바꿔서 진행했더니 된다는 글들이 있습니다.
제 경우 브라우저를 바꿔서 다시 진행해도 결과는 동일했고, 데이터를 모두 지우고 https://로 재접속하니 해결되었습니다.
관련 글
2025.01.23 - [Apps] - Authentik으로 홈서버 SSO 구현하기 (2) – LDAP
Authentik으로 홈서버 SSO 구현하기 (2) – LDAP
2025년 1월 6일 변경사항 : LDAP Provider 변경된 버전에 맞춰 내용 수정 LDAP 구성하기실생활에서 LDAP를 제일 쉽게 접할 수 있는 곳은 회사입니다. 로그인, 네트워크 장치 정보 등을 중앙에 모아놓고
worklazy.net
2025.01.23 - [Apps] - Authentik으로 홈서버 SSO 구현하기 (3) – OAuth/OpenID
Authentik으로 홈서버 SSO 구현하기 (3) – OAuth/OpenID
개요LDAP는 있으면 좋지만, 이것만으로는 번거로운 로그인 과정을 전부 해결할 수는 없습니다.이번 글에서는 OAuth / OpenID Provider를 설정하는 방법을 적어 보겠습니다.LDAP가 조직 내부 네트워크 인
worklazy.net
2025.01.23 - [Apps] - Authentik으로 홈서버 SSO 구현하기 (4) – SAML
Authentik으로 홈서버 SSO 구현하기 (4) – SAML
개요SAML은 JWT기반이었던 OAuth/OpenID와 달리 XML을 기반으로 하며 각각 장단점이 있습니다. OAuth/OpenID와 비교할 때 모바일 지원이 부족하고, JWT보다 덩치가 큰 XML을 이용함에서 오는 네트워크 대역
worklazy.net
2025.01.23 - [Apps] - Authentik으로 홈서버 SSO 구현하기 (5) - Proxy
Authentik으로 홈서버 SSO 구현하기 (5) - Proxy
개요홈서버에서 사용하는 대부분의 애플리케이션들은 앞선 글에서 소개했던 OAuth/OpenID, SAML로 모두 해결할 수 있습니다.그러나, 이런 류의 인증방식을 지원하지 않는 것들도 굉장히 많죠. 예를
worklazy.net
2025.01.23 - [Apps] - Authentik으로 홈서버 SSO 구현하기 (6) – 회원 가입, 초대 코드, 소셜 로그인
Authentik으로 홈서버 SSO 구현하기 (6) – 회원 가입, 초대 코드, 소셜 로그인
개요Authentik의 Provider를 이용해 많은 서비스를 연동하는 것까진 완료했지만, 아직 회원을 받는 기능은 없습니다.주변 지인들에게 계정을 발급할 때, 하나하나 생성해 주어야 하는 귀찮은 점은 아
worklazy.net
2025.01.23 - [Apps] - Authentik으로 홈서버 SSO 구현하기 (7) – 시놀로지 회원 가입(계정 자동 생성) 구현하기
Authentik으로 홈서버 SSO 구현하기 (7) – 시놀로지 회원 가입(계정 자동 생성) 구현하기
개요시놀로지는 다 좋고 편한데 회원 가입을 능동적으로 처리하는 기능이 없습니다. 기본 기능만을 사용할 경우, 관리자가 계정을 생성하고 사용자가 최초 로그인 시 무조건 비밀번호를 변경하
worklazy.net
출처
1. https://docs.goauthentik.io/docs/install-config/install/docker-compose
Docker Compose installation | authentik
This installation method is for test setups and small-scale production setups.
docs.goauthentik.io
2. https://docs.goauthentik.io/docs/sys-mgmt/certificates
Certificates | authentik
Certificates in authentik are used for the following use cases:
docs.goauthentik.io