8 min read

Pangolin + Authentik으로 홈서버 SSO 구축하기

진짜 너무 불편하네..

홈서버에 서비스를 하나둘 올리다 보면 어느 순간 깨닫습니다. "나 지금 몇 개나 로그인하고 있는 거지?"

파일 관리자, 다운로드 클라이언트, 모니터링 대시보드, DNS 관리 패널… 각각 자체 인증을 갖고 있고, 비밀번호도 제각각이고, 세션 만료 시점도 다릅니다. 외부에서 접속할 때마다 서비스별로 로그인을 반복하는 게 점점 귀찮아졌어요.

"한 번 로그인하면 전부 통하게 할 수 없을까?" 가 출발점이었습니다.
사실 이미 OIDC 같은 개념이 있는건 충분히 알고 있으니 이제 홈서버에 적용하는 일만 남은거죠.


선택지: Authelia vs Authentik

셀프호스팅 SSO 솔루션으로 가장 많이 언급되는 두 가지가 있습니다.

Authelia — 가볍고 설정이 YAML 기반. 2FA 지원. 단순한 ForwardAuth 용도로는 최적. 하지만 OIDC Provider 기능이 제한적이고, 관리 UI가 없어서 설정 변경할 때마다 파일을 수정하고 재시작해야 합니다.

Authentik — 무겁지만 풀스택 IdP. OIDC/SAML/LDAP 전부 지원. 웹 UI에서 Provider, Application, Flow를 관리할 수 있고, 사용자/그룹 관리도 됩니다. 대신 PostgreSQL이 필요하고 메모리를 꽤 먹어요.

저는 Authentik을 골랐습니다. 이유는:

  1. Pangolin이 OIDC IdP 연동을 네이티브로 지원함
  2. 나중에 서비스별로 세밀한 접근 제어가 필요할 수 있음 (그룹별 허용 등)
  3. 관리 UI가 있어야 설정 바꿀 때 SSH 안 타도 됨

메모리 부담이 걱정이었는데, server + worker + postgres 합쳐서 약 400~500MB 정도. 8GB RAM 서버에서 감당 가능한 수준이었습니다.


전체 구조

1.png

(이번에도 GPT 가 열일해서 도식도를 잘 그려줬네요...)

핵심은 Badger라는 Traefik 플러그인입니다. Pangolin이 자체적으로 만든 건데, 요청이 들어올 때 세션 쿠키를 확인하고, 유효하지 않으면 Pangolin 로그인 페이지로 보냅니다. 기존의 ForwardAuth 방식과 비슷하지만, Pangolin 생태계 안에서 더 깔끔하게 동작해요.


Step 1: Authentik 배포

Docker Compose로 올립니다. 구조는 단순해요.

# docker-compose.yml (예시)
services:
  postgresql:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: authentik
      POSTGRES_USER: authentik
      POSTGRES_PASSWORD: ${PG_PASS}
    volumes:
      - db_data:/var/lib/postgresql/data

  server:
    image: ghcr.io/goauthentik/server:2025.10
    command: server
    environment:
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_SECRET_KEY: ${SECRET_KEY}
    ports:
      - "7080:9000"
    depends_on:
      postgresql:
        condition: service_healthy

  worker:
    image: ghcr.io/goauthentik/server:2025.10
    command: worker
    environment:
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_SECRET_KEY: ${SECRET_KEY}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

주의할 점: server 컨테이너는 리버스 프록시 네트워크에 붙여야 합니다. Traefik이 Authentik에 접근할 수 있어야 하니까요. worker는 내부 네트워크만 있으면 됩니다.

초기 설정은 http://서버IP:7080/if/flow/initial-setup/으로 접속해서 관리자 계정을 만듭니다.


Step 2: Authentik에서 OIDC Provider 생성

Authentik 관리 화면에서:

  1. Provider 생성 (OAuth2/OpenID Provider)

    • Name: Pangolin SSO
    • Authorization flow: default-provider-authorization-implicit-consent (매번 동의 안 물어보게)
    • Client ID: 자동 생성됨 (복사해둘 것)
    • Client Secret: 자동 생성됨 (복사해둘 것)
    • Redirect URI: https://pangolin.example.com/api/v1/auth/idp/oidc/2/callback
  2. Application 생성

    • Name: Pangolin
    • Slug: pangolin
    • Provider: 위에서 만든 Pangolin SSO 선택

여기서 Redirect URI가 중요합니다. Pangolin이 IdP를 등록하면 콜백 URL을 알려주는데, 그걸 정확히 맞춰야 해요. 안 맞으면 인증 후 "redirect_uri mismatch" 에러가 납니다.


Step 3: Pangolin에 IdP 등록

Pangolin 대시보드 → Settings → Identity Providers에서 OIDC를 추가합니다.

  • Name: Sign in with Authentik
  • Client ID / Secret: Authentik에서 복사한 값
  • Auth URL: https://auth.example.com/application/o/authorize/
  • Token URL: https://auth.example.com/application/o/token/
  • Identifier Path: email
  • Scopes: openid profile email
  • Auto Provision: 켜기 (Authentik 계정이 있으면 Pangolin에 자동 등록)

등록하면 Pangolin 로그인 페이지에 "Sign in with Authentik" 버튼이 생깁니다.


Step 4: 리소스별 SSO 적용

Pangolin에서 리소스(서비스)를 만들 때 SSO 보호를 켜면, 해당 서브도메인에 접근할 때 Badger 플러그인이 세션을 확인합니다.

예를 들어:

  • files.example.com → SSO 켜짐 → 로그인 필요
  • blog.example.com → SSO 꺼짐 → 누구나 접근 가능

SSO를 켜면 안 되는 서비스도 있습니다:

  • Authentik 자체 (로그인 페이지에 접근을 못하면 순환 참조)
  • API를 외부에서 호출하는 서비스 (Ghost API 등)
  • WebDAV/SFTP처럼 브라우저가 아닌 클라이언트가 접근하는 서비스
서비스 SSO 이유
파일 관리자 민감한 데이터
다운로드 클라이언트 외부 노출 위험
모니터링 대시보드 서버 정보 노출
DNS 관리 설정 변경 가능
블로그 공개 콘텐츠
음악 공유 앱 자체 인증 있음
이력서 공개 페이지

하다보니.. Ghost API와 SSO 충돌

블로그에 SSO를 켰더니 Ghost Admin API 호출이 전부 막혔습니다. 자동 발행 스크립트가 401을 뱉더라고요.

Pangolin의 Resource Rules로 해결했습니다. 특정 경로만 SSO를 우회하는 규칙을 추가할 수 있어요.

규칙 1: /ghost/api/* → PASS (SSO 우회)
규칙 2: /ghost/*     → ACCEPT (SSO 적용)  
규칙 3: *            → ACCEPT (SSO 적용)

우선순위가 중요합니다. API 경로를 먼저 매칭해서 통과시키고, 나머지 Ghost 관리 페이지는 SSO로 보호합니다. 이 순서가 바뀌면 API도 막혀버려요.


ForwardAuth vs Badger: 뭐가 다른가

Pangolin 이전에는 Traefik + ForwardAuth 미들웨어로 SSO를 구현하는 게 일반적이었습니다.

# ForwardAuth 방식 (예시)
middlewares:
  authentik:
    forwardAuth:
      address: "http://authentik:9000/outpost.goauthentik.io/auth/traefik"
      authResponseHeaders:
        - X-authentik-username
        - X-authentik-groups
        - X-authentik-email
      trustForwardHeader: true

이 방식도 동작하지만, 서비스마다 미들웨어를 수동으로 붙여야 합니다. Traefik 동적 설정 파일을 수정하거나, Docker 라벨을 추가하거나.

Pangolin + Badger 방식은 UI에서 토글 하나로 SSO를 켜고 끌 수 있습니다. 내부적으로는 비슷한 일을 하지만, 관리 포인트가 Pangolin 대시보드 하나로 모입니다.

다만 Pangolin의 Badger 방식은 Pangolin 생태계에 종속됩니다. Traefik을 직접 관리하는 게 편한 사람은 ForwardAuth가 나을 수도 있어요. 저는 서비스가 20개 가까이 되니까 UI에서 관리하는 게 압도적으로 편했습니다.


CrowdSec 연동: SSO 위에 한 겹 더

SSO만으로는 부족한 부분이 있습니다. 로그인 페이지 자체에 대한 브루트포스 공격이나, 알려진 악성 IP의 접근을 막으려면 별도 프로텍션 레이어가 필요해요.

CrowdSec을 Traefik 플러그인으로 붙였습니다. 커뮤니티 기반 IP 블랙리스트 + 로컬 로그 분석으로 의심스러운 접근을 차단합니다.

이것만 하더라도 귀찮은 탐지봇 녀석들 엿을 먹여줄 수 있습니다..

# Traefik 동적 설정 (예시)
middlewares:
  crowdsec:
    plugin:
      crowdsec-bouncer:
        crowdsecLapiScheme: http
        crowdsecLapiHost: crowdsec:8080
        crowdsecLapiKey: ${BOUNCER_API_KEY}

Pangolin의 Middleware Manager를 통해 특정 리소스에만 CrowdSec을 적용할 수도 있습니다. 저는 외부 노출되는 서비스 전부에 걸어뒀어요. 음.. 이런게 왜 필요한지에 대한 상세는
나중에 또 다른 포스팅에서 다뤄보면 좋을거같습니다.


돌이켜보면

좋았던 점: 한 번 로그인하면 SSO가 적용된 모든 서비스에 세션이 유지됩니다. 모바일에서도 브라우저 하나로 전부 접근 가능. 새 서비스 추가할 때 토글 하나면 끝.

아쉬웠던 점: Authentik이 생각보다 무겁습니다. 초기 설정도 Provider → Application → Pangolin IdP 등록 → 콜백 URL 맞추기까지 삽질이 좀 있었어요. 특히 Redirect URI 불일치 에러는 디버깅이 짜증났습니다.

다시 한다면: 서비스가 5개 이하였으면 Authelia로 갔을 겁니다. 가볍고 설정도 단순하니까요. 하지만 10개 넘어가면 Authentik의 웹 UI와 OIDC 네이티브 지원이 확실히 편합니다. 서비스 수에 따라 선택이 달라지는 문제라고 생각해요.


참고 자료