SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-21

oauth2-proxy로 모든 앱에 SSO 적용하기

로그인 기능이 없는 애플리케이션을 OIDC 기반 SSO 환경으로 전환하는 방법을 설명합니다. Nginx와 Traefik에서 Forward Auth를 설정하여 쿠키와 리다이렉트 오류를 방지하고, 안전하게 인증을 처리하는 구체적인 아키텍처와 보안 주의사항을 확인하십시오.

Forward auth: 로그인 기능이 없는 애플리케이션에 SSO 적용하기

oauth2-proxy를 사용하면 자체 로그인 기능이 없는 애플리케이션에도 싱글 사인온(SSO)을 적용할 수 있습니다. 이 방식은 애플리케이션 앞단에 위치한 리버스 프록시가 모든 요청을 가로채는 원리로 작동합니다. 프록시는 oauth2-proxy에 해당 요청이 유효한 세션을 포함하고 있는지 확인한 뒤, 응답이 긍정적일 때만 요청을 업스트림으로 전달합니다. 애플리케이션은 이러한 확인 과정을 인지하지 못하므로 코드를 수정할 필요가 없습니다.

이 확인 과정은 추가적인 HTTP 요청을 통해 이루어집니다. 프록시는 들어온 요청 헤더의 복사본을 /oauth2/auth로 보내고 응답 상태 코드를 확인합니다. 상태 코드가 202이면 호출자에게 유효한 세션이 있다는 의미이므로, 프록시는 원래 요청을 애플리케이션으로 전달합니다. 401이면 세션이 없다는 의미이므로, 프록시는 브라우저를 /oauth2/sign_in로 리다이렉트하여 사용 중인 ID 공급자(Identity Provider)를 통한 OpenID Connect(OIDC) 로그인을 시작합니다. OIDC는 OAuth 2.0 기반의 ID 계층이며, 공급자는 이미 로그인용으로 운영 중인 서비스를 그대로 사용하면 됩니다.

이 패턴은 모든 리버스 프록시에서 지원합니다. Nginx에서는 auth_request 지시어를 사용합니다. Traefik에서는 forwardAuth 미들웨어를 사용합니다. Caddy에서는 forward_auth로 설정합니다. 서브 요청에 응답하는 서비스 또한 교체 가능합니다. oauth2-proxy는 별도의 데이터베이스가 필요 없고 표준 OIDC를 지원하기 때문에 가장 널리 사용됩니다.

설정을 작성하기 전에 신뢰 경계를 설정하십시오

성공적으로 확인을 마치면 oauth2-proxy는 식별 정보를 응답 헤더로 반환하며, 리버스 프록시는 이를 업스트림 요청에 복사합니다. set_xauthrequest를 활성화하면 X-Auth-Request-UserX-Auth-Request-Email를 얻게 됩니다. 애플리케이션은 해당 헤더를 읽고 이를 신뢰합니다.

이것이 보안 모델의 전부이므로, 그 결과를 명확히 인지해야 합니다. 애플리케이션 포트로 TCP 연결을 맺을 수 있는 모든 주체는 스스로 해당 헤더를 설정하여 임의의 사용자가 될 수 있습니다. 단 하나의 curl -H "X-Auth-Request-Email: admin@example.com" http://app-host:3000/라도 애플리케이션에 직접 도달하면 보안이 완전히 우회됩니다.

따라서 애플리케이션은 프록시를 통하지 않고는 접근할 수 없어야 합니다. Docker Compose를 사용하는 경우, 앱 서비스에서 ports: 매핑을 삭제하고 내부 네트워크에만 남겨두어 프록시 컨테이너만 해당 앱에 연결할 수 있도록 하십시오. 베어메탈 호스트에서는 앱을 0.0.0.0:3000 대신 127.0.0.1:3000에 바인딩하십시오. 그런 다음 실제로 무엇이 노출되었는지 확인하십시오.

sudo ss -tlnp | grep 3000

0.0.0.0:3000라고 적힌 줄은 앱이 공인 IP에서 응답하고 있으며, 게이트웨이가 장식에 불과함을 의미합니다. 127.0.0.1:3000이 우리가 원하는 상태입니다. 방화벽 규칙은 유용한 2차 방어선이지만, 바인딩 주소는 다른 도구가 규칙 세트를 초기화하더라도 유지되는 설정입니다.

oauth2-proxy 설치

2026년 8월 기준 최신 릴리스는 2026년 6월에 배포된 v7.15.3입니다. 바이너리를 설치하고 다운로드 파일을 검증하십시오:

cd /tmp
curl -fsSLO https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.15.3/oauth2-proxy-v7.15.3.linux-amd64.tar.gz
curl -fsSLO https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.15.3/oauth2-proxy-v7.15.3.linux-amd64.tar.gz-sha256sum.txt
sha256sum -c oauth2-proxy-v7.15.3.linux-amd64.tar.gz-sha256sum.txt
tar -xzf oauth2-proxy-v7.15.3.linux-amd64.tar.gz
sudo install -m 755 oauth2-proxy-v7.15.3.linux-amd64/oauth2-proxy /usr/local/bin/oauth2-proxy
oauth2-proxy --version

sha256sum -c은(는) OK로 끝나는 줄을 출력해야 합니다. 만약 FAILED이(가) 출력된다면, 바이너리를 실행하지 말고 중단한 뒤 다시 다운로드하십시오.

Docker의 경우 이미지는 quay.io/oauth2-proxy/oauth2-proxy이며, 태그를 고정해야 합니다: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3. 태그를 latest으로 두면 일상적인 docker compose pull 작업이 서버 내 모든 애플리케이션을 보호하는 프로세스의 예기치 않은 업그레이드로 이어질 수 있습니다.

쿠키 시크릿 생성

세션 쿠키는 암호화되며, cookie_secret이 그 키 역할을 합니다. 이 값은 AES(Advanced Encryption Standard) 키로 사용되므로 반드시 16, 24 또는 32바이트여야 합니다. 다른 길이를 사용하면 oauth2-proxy는 시작 시 쿠키 시크릿과 관련된 오류를 발생시키며 실행되지 않습니다.

openssl rand -base64 32 | tr -- '+/' '-_'

tr는 단순히 보기 좋으라고 사용하는 것이 아닙니다. 이 명령은 일반 base64 값을 URL 안전 알파벳으로 변환하여, 셸, 환경 변수 파일, HTTP 헤더 등에서 따옴표 문제 없이 값이 전달되도록 합니다.

이 값에는 두 가지 규칙이 있습니다. 배포할 때마다 서로 다른 시크릿을 사용하십시오. 동일한 도메인 뒤에서 여러 개의 oauth2-proxy 인스턴스를 실행하는 경우에는 모두 동일한 시크릿을 설정해야 합니다. 한 인스턴스에서 암호화한 쿠키를 다른 인스턴스에서도 읽을 수 있어야 하기 때문입니다.

oauth2-proxy 설정 작성하기

긴 명령줄 대신 설정 파일을 사용하면 ps 출력에 client secret이 노출되지 않습니다.

# /etc/oauth2-proxy/oauth2-proxy.cfg
http_address = "127.0.0.1:4180"
reverse_proxy = true

provider = "oidc"
oidc_issuer_url = "https://id.example.com/application/o/myapp/"
client_id = "REPLACE_ME"
client_secret = "REPLACE_ME"

redirect_url = "https://app.example.com/oauth2/callback"
cookie_secret = "REPLACE_ME"
cookie_secure = true
cookie_domains = [".example.com"]
whitelist_domains = [".example.com"]

email_domains = ["*"]
set_xauthrequest = true
upstreams = ["static://202"]

reverse_proxy = true 옵션은 oauth2-proxy가 앞단 프록시에서 전달한 X-Forwarded-* 헤더를 신뢰하도록 설정합니다. 이 옵션이 없으면 oauth2-proxy는 프록시의 주소를 클라이언트 주소로 간주하며, 요청이 HTTPS를 통해 전달되었는지 잘못 판단할 수 있습니다.

upstreams = ["static://202"] 옵션을 사용하면 oauth2-proxy는 인증된 요청에 대해 202 응답을 반환하고 별도의 프록시 처리를 하지 않습니다. 리버스 프록시가 실제 프록시 역할을 수행하는 forward auth 구성에서는 이 방식이 필수적입니다. 다른 배포 방식은 upstreams = ["http://127.0.0.1:3000"]를 사용하여 oauth2-proxy를 요청 경로에 직접 배치하며, 이때는 auth_request를 사용하지 않습니다. 이 방식은 단일 애플리케이션에는 간단하지만, 서비스가 열 개 이상으로 늘어나면 확장성이 떨어집니다.

email_domains = ["*"] 옵션은 인증 제공자가 허용할 모든 주소를 승인합니다. 이 범위를 자신의 도메인으로 좁히거나, 더 나아가 인증 제공자 측에서 그룹 바인딩을 통해 접근을 제한하는 것이 좋습니다. 사용자 관리는 인증 제공자에서 수행하는 것이 효율적이기 때문입니다.

systemd를 사용하여 별도의 사용자 권한으로 실행하십시오:

# /etc/systemd/system/oauth2-proxy.service
[Unit]
Description=oauth2-proxy
After=network-online.target
Wants=network-online.target

[Service]
User=oauth2-proxy
Group=oauth2-proxy
ExecStart=/usr/local/bin/oauth2-proxy --config=/etc/oauth2-proxy/oauth2-proxy.cfg
Restart=on-failure
ProtectSystem=strict
PrivateTmp=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
sudo useradd --system --no-create-home --shell /usr/sbin/nologin oauth2-proxy
sudo install -d -m 750 /etc/oauth2-proxy
sudo chown -R oauth2-proxy:oauth2-proxy /etc/oauth2-proxy
sudo chmod 600 /etc/oauth2-proxy/oauth2-proxy.cfg
sudo systemctl daemon-reload
sudo systemctl enable --now oauth2-proxy
curl -s http://127.0.0.1:4180/ping

/pingOK를 출력한다면 프로세스가 정상적으로 시작되어 설정을 불러왔음을 의미합니다. 이는 oauth2-proxy 자체의 상태 확인 엔드포인트이며 세션을 요구하지 않습니다. 만약 응답이 없다면 journalctl -u oauth2-proxy -n 50를 확인하십시오. 잘못된 issuer URL이나 길이가 맞지 않는 cookie secret은 시작 단계에서 실패를 유발하며, 로그에 그 원인이 기록됩니다.

제공업체에 리다이렉트 URI 등록하기

제공업체에서 OIDC 애플리케이션을 생성하고, 해당 리다이렉트 URI를 설정 파일의 redirect_urlhttps://app.example.com/oauth2/callback와 정확히 일치하도록 설정하십시오. 정확히 일치한다는 것은 스킴(scheme), 호스트, 포트, 경로가 문자 하나까지 모두 같아야 함을 의미합니다. 마지막에 슬래시(/)가 하나만 추가되어도 다른 URI로 간주됩니다.

이는 전체 설정 과정에서 가장 흔히 발생하는 오류이며, oauth2-proxy가 관여하기도 전에 실패합니다. 제공업체가 인증 요청을 거부하고 자체 오류 페이지를 표시하므로, oauth2-proxy 로그에는 아무것도 나타나지 않습니다. 이때 브라우저 주소창을 확인하면 여전히 제공업체의 도메인에 머물러 있으며, 쿼리 문자열에 error=invalid_request가 포함되어 있거나 redirect_uri와 같은 페이지 이름이 직접 나타납니다. 이러한 현상이 보이면 프록시 설정이 아닌 제공업체의 애플리케이션 레코드를 수정하십시오.

발급자(issuer) URL은 직접 입력하지 말고 제공업체에서 복사하십시오. oauth2-proxy는 oidc_issuer_url 뒤에 /.well-known/openid-configuration를 붙여 시작 시점에 해당 검색(discovery) 문서를 가져옵니다. 먼저 직접 확인해 보십시오:

curl -s https://id.example.com/application/o/myapp/.well-known/openid-configuration | head -c 400

JSON 결과에 authorization_endpoint 키가 포함되어 있다면 발급자 URL이 올바른 것입니다. 404 오류나 HTML 오류 페이지가 반환된다면 URL이 잘못된 것이며, oauth2-proxy도 동일한 404 오류로 인해 시작에 실패할 것입니다. 아직 제공업체를 선택하지 않았다면 Keycloak, Authentik, Zitadel 비교 문서를 통해 장단점을 확인하고, Authentik을 자체 SSO 서버로 운영하기 문서를 통해 이 설정의 제공업체 측 구성을 단계별로 따라 할 수 있습니다.

Nginx: auth_request

Nginx는 auth_request을 사용하여 포워드 인증(forward auth)을 수행합니다. 이 기능은 내부 서브 리퀘스트를 발생시키고 그 상태 코드에 따라 분기 처리를 합니다.

# in the http context, next to your other maps
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
  listen 443 ssl;
  server_name app.example.com;

  location /oauth2/ {
    proxy_pass       http://127.0.0.1:4180;
    proxy_set_header Host                    $host;
    proxy_set_header X-Real-IP               $remote_addr;
    proxy_set_header X-Auth-Request-Redirect $request_uri;
  }

  location = /oauth2/auth {
    proxy_pass       http://127.0.0.1:4180;
    proxy_set_header Host             $host;
    proxy_set_header X-Real-IP        $remote_addr;
    proxy_set_header X-Forwarded-Uri  $request_uri;
    proxy_set_header Content-Length   "";
    proxy_pass_request_body           off;
  }

  location / {
    auth_request /oauth2/auth;
    error_page 401 = @oauth2_signin;

    auth_request_set $user  $upstream_http_x_auth_request_user;
    auth_request_set $email $upstream_http_x_auth_request_email;
    proxy_set_header X-User  $user;
    proxy_set_header X-Email $email;

    auth_request_set $auth_cookie $upstream_http_set_cookie;
    add_header Set-Cookie $auth_cookie;

    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host       $host;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
  }

  location @oauth2_signin {
    return 302 /oauth2/sign_in?rd=$scheme://$host$request_uri;
  }
}

위 설정에서 세 가지 세부 사항은 반드시 챙겨야 합니다. Content-Length를 비워 둔 proxy_pass_request_body off은 모든 POST 요청의 본문을 서브 리퀘스트로 복사하지 않도록 합니다. oauth2-proxy는 본문을 읽지 않으므로 이 설정은 중요합니다. 이 설정을 하지 않으면 파일 업로드 시 파일이 두 번 전송됩니다.

auth_request_set $auth_cookieadd_header Set-Cookie 쌍은 갱신된 세션 쿠키를 브라우저로 다시 전달합니다. 이 설정을 생략하면 cookie_refresh는 아무런 동작을 하지 않습니다. Nginx가 서브 리퀘스트의 Set-Cookie을 폐기하기 때문에 브라우저는 세션이 만료될 때까지 이전 값을 유지하게 됩니다.

error_page 401 = @oauth2_signin는 인증 실패 시 로그인 페이지로 전환하는 역할을 합니다. 이 설정이 없으면 인증되지 않은 방문자는 단순히 401 Authorization Required 페이지만 보게 되며, 다음 단계로 진행할 방법이 없습니다.

설정을 다시 불러오기(reload) 전에 항상 테스트하십시오:

sudo nginx -t && sudo systemctl reload nginx

주변 지시어(directive)가 생소하다면, Nginx 리버스 프록시 설정의 구조에서 이 설정의 기반이 되는 계층을 다루고 있으니 참고하십시오.

Traefik: forwardAuth 미들웨어

Traefik은 동일한 작업을 수행하기 위해 두 개의 미들웨어가 필요합니다. 하나는 인증 확인을 수행하고, 다른 하나는 401 상태 코드를 브라우저 리다이렉트로 변환합니다.

# dynamic configuration
http:
  middlewares:
    oauth-auth:
      forwardAuth:
        address: https://oauth.example.com/oauth2/auth
        trustForwardHeader: true
    oauth-errors:
      errors:
        status:
          - "401-403"
        service: oauth-backend
        query: "/oauth2/sign_in?rd={url}"
        statusRewrites:
          "401": 302

두 미들웨어를 애플리케이션 앞단에 위치한 라우터에 연결하십시오. 또한 oauth2-proxy는 별도의 라우터에서 oauth.example.com로 게시해야 합니다. 브라우저가 인증 확인을 거치지 않고 /oauth2/sign_in/oauth2/callback에 도달할 수 있어야 하기 때문입니다.

statusRewrites에서 401을 302로 매핑하는 과정은 사용자들이 자주 놓치는 부분입니다. 이 설정이 없으면 Traefik은 401 상태 코드와 함께 로그인 리다이렉트를 반환하는데, 브라우저는 이를 따르지 않으며 방문자는 Found.이라는 단어만 포함된 페이지를 보게 됩니다.

trustForwardHeader: true은 원래의 호스트와 URI를 oauth2-proxy로 전달합니다. oauth2-proxy는 사용자가 요청했던 페이지로 다시 돌아가게 할 rd 값을 생성하기 위해 이 정보가 필요합니다. whitelist_domains을 설정하여 해당 호스트를 포함시키십시오. 그렇지 않으면 oauth2-proxy는 오픈 리다이렉트 위험을 이유로 rd 매개변수를 삭제하며, 모든 사용자는 로그인 후 /로 이동하게 됩니다. Traefik 서버는 보통 여러 애플리케이션을 동시에 처리하므로, 하나의 Traefik 인스턴스를 통해 여러 Docker Compose 앱 라우팅하기 문서를 참조하여 이 설정이 적용되는 라우터 구조를 확인하십시오.

Caddy: forward_auth

app.example.com {
  handle /oauth2/* {
    reverse_proxy oauth2-proxy.internal:4180 {
      header_up X-Real-IP {remote_host}
      header_up X-Forwarded-Uri {uri}
    }
  }
  handle {
    forward_auth oauth2-proxy.internal:4180 {
      uri /oauth2/auth
      header_up X-Real-IP {remote_host}
      copy_headers X-Auth-Request-User X-Auth-Request-Email
      @error status 401
      handle_response @error {
        redir * /oauth2/sign_in?rd={scheme}://{host}{uri}
      }
    }
    reverse_proxy upstream.internal:3000
  }
}

여기서는 순서가 중요합니다. /oauth2/* 블록이 가장 먼저 와야 하며, 여기에는 forward_auth이 포함되지 않아야 합니다. 로그인하지 않은 방문자가 로그인 및 콜백 경로에 도달할 수 있어야 하기 때문입니다. 해당 경로 앞에 검사 로직을 배치하면 브라우저가 포기할 때까지 로그인 리다이렉트가 무한 반복됩니다.

copy_headers은 업스트림 요청에 신원 정보를 전달하는 역할을 하며, oauth2-proxy가 set_xauthrequest = true 옵션으로 실행될 때만 값을 생성합니다. 어떤 프록시를 선택할지는 별개의 문제이며, nginx, Caddy 및 Traefik 비교 문서에서 이를 다룹니다.

로그인 페이지로 계속 돌아오는 이유는 무엇입니까?

제공자(provider)에서 로그인한 뒤 다시 돌아왔을 때, oauth2-proxy가 즉시 제공자로 다시 리다이렉트하는 경우입니다. 이 루프는 oauth2-proxy가 나가는 길에 설정한 쿠키 없이 콜백 요청이 도착했음을 의미합니다. 로그에는 다음과 같이 기록됩니다.

No cookies were found in OAuth callback.

또는 다른 쿠키는 전달되었으나 올바른 쿠키가 전달되지 않은 경우입니다.

Cookies were found in OAuth callback, but none was a CSRF cookie.

CSRF는 사이트 간 요청 위조(cross-site request forgery)를 의미하며, 이 쿠키는 콜백을 시작된 로그인 세션과 연결하기 위해 존재합니다. 브라우저에서는 동일한 실패가 다음과 같이 보입니다.

Login Failed: Unable to find a valid CSRF token. Please try again.

다음 네 가지 원인을 순서대로 확인하십시오.

  1. 브라우저가 일반 HTTP로 사이트에 접속하는 동안 cookie_secure = true이 설정된 경우입니다. 브라우저는 Secure로 표시된 쿠키를 http:// 출처에 저장하지 않으므로, 쿠키가 다시 전송되지 않습니다. TLS(transport layer security)를 올바르게 종료하거나, localhost에서 테스트하는 경우에만 cookie_secure = false을 설정하십시오.
  2. 주소 표시줄의 호스트 이름을 포함하지 않는 cookie_domains 값입니다. .example.comapp.example.com을 포함하지만 app.example.net에 대해서는 아무런 동작을 하지 않습니다.
  3. 브라우저가 쿠키를 삭제하는 경우입니다. 엄격한 개인정보 보호 확장 프로그램이나 타사 쿠키 차단 설정이 아웃바운드 리다이렉트와 콜백 사이에서 _oauth2_proxy_csrf을 제거할 수 있습니다.
  4. 서버 시간 오차입니다. 서버 시간이 제공자의 시간과 크게 차이 나면, ID 토큰의 iatexp이 허용 범위를 벗어나 세션이 거부됩니다. timedatectlSystem clock synchronized: yes를 보고해야 합니다.

추측하지 말고 서버 측에서 발생하는 상황을 모니터링하십시오.

sudo journalctl -u oauth2-proxy -f

시크릿 창에서 앱을 로드하십시오. 모든 요청은 상태와 함께 기록되므로, 콜백 직후 제공자로 다시 리다이렉트되는 기록이 있다면 그것이 바로 루프입니다.

로그인을 건너뛰어야 하는 경로: API, 웹훅, 웹소켓

Forward auth는 브라우저가 쿠키를 보유하고 있다고 가정합니다. 브라우저가 없는 호출자는 인증에 실패합니다.

Authorization: Bearer <token>를 보내는 API 클라이언트는 쿠키가 없으므로 공급자의 로그인 페이지로 302 리다이렉트를 받게 되며, 이후 HTML을 JSON으로 파싱하려고 시도합니다. 이를 해결하는 깔끔한 방법은 두 가지입니다. skip_jwt_bearer_tokens = true를 설정하면 oauth2-proxy가 동일한 발급자로부터 받은 유효한 JWT(JSON web token) Bearer 토큰을 수락하게 되는데, 이는 API 클라이언트가 이미 공급자로부터 토큰을 발급받고 있는 경우에 적합합니다. 그렇지 않다면 해당 경로를 예외 처리하십시오.

skip_auth_routes = [
  "^/api/",
  "POST=^/webhook/",
  "GET=^/healthz$"
]

각 값은 정규 표현식이며 정규화된 경로와 대조됩니다. 선택적으로 HTTP 메서드와 =를 접두사로 붙일 수 있습니다. POST=^/webhook/을 사용하면 웹훅 수신기는 POST 요청에 대해 열려 있는 상태를 유지하면서, 동일한 경로로 접속하는 일반 사용자는 로그인 페이지로 이동하게 할 수 있습니다. 모든 항목은 보안상의 구멍이 될 수 있으므로 ^을 사용하여 표현식을 고정하고 호출자가 허용하는 범위 내에서 최대한 좁게 유지하십시오.

웹소켓은 사람들이 가장 흔히 잘못 이해하는 사례입니다. 업그레이드 요청은 일반적인 HTTP GET 요청이며 다른 요청과 동일한 쿠키를 전달하므로, 별도의 예외 처리 없이도 정상적으로 검증을 통과합니다. 문제가 발생하는 지점은 그 주변의 프록시 설정입니다. 보호된 위치에 UpgradeConnection 헤더가 없으면 업그레이드가 완료되지 않으며, 앱 클라이언트는 브라우저 콘솔에 WebSocket connection ... failed 메시지를 띄우며 무한 재시도를 반복합니다. 요청은 이미 인증된 상태이므로 해당 경로를 예외 처리하는 것은 아무런 도움이 되지 않습니다.

실질적인 제한 사항이 하나 있습니다. 검증은 업그레이드 시점에 단 한 번만 수행됩니다. 몇 시간 동안 열려 있는 웹소켓은 다시 검증되지 않으므로, 공급자 측에서 사용자를 삭제하더라도 이미 연결된 소켓은 닫히지 않습니다. 활성 연결을 끊으려면 앱을 재시작해야 합니다.

세션 저장소와 비대해지는 쿠키 문제

기본적으로 전체 세션은 cookie_secret로 암호화되어 쿠키 내부에 저장됩니다. 이 방식은 oauth2-proxy를 상태 비저장(stateless)으로 유지하며 별도의 서비스가 필요하지 않습니다. 하지만 브라우저의 쿠키 크기가 약 4 KB로 제한되므로 용량 한계가 존재합니다. ID 토큰에 긴 그룹 클레임 목록이 포함되면 oauth2-proxy는 세션을 _oauth2_proxy_0, _oauth2_proxy_1 등으로 분할하며, 조각이 늘어나 요청 헤더가 커지면 애플리케이션이 요청을 받기도 전에 nginx가 400 Request Header Or Cookie Too Large를 반환하게 됩니다.

이런 상황이 발생하면 세션을 서버 측에 저장하도록 변경하십시오.

session_store_type = "redis"
redis_connection_url = "redis://127.0.0.1:6379"

이제 브라우저는 짧은 티켓만 보유하고 암호화된 세션은 Redis에 저장됩니다. 이 방식은 서비스를 항상 가동해야 한다는 비용이 따릅니다. Redis가 중단되면 모든 세션이 무효화되어 모든 사용자가 즉시 로그아웃됩니다. 쿠키 저장소 방식에도 단점은 있습니다. 동일한 세션을 동시에 갱신하는 두 개의 요청이 충돌하여 재로그인을 강제할 수 있다는 점입니다.

Forward auth가 제공하지 않는 것

이는 문 앞의 게이트일 뿐입니다. 애플리케이션 내부의 권한 부여가 아니며, 이 차이가 해당 방식이 귀하의 사례에 적합한지 결정합니다.

사용자가 통과하면 애플리케이션은 평소와 동일한 정보를 확인합니다. 애플리케이션 자체적인 역할(role) 체계가 있더라도, 애플리케이션이 헤더 기반 인증을 지원하고 헤더를 계정에 매핑하지 않는 한 Forward auth는 이를 채워주지 않습니다. Grafana는 auth.proxy 설정을 통해 이를 지원합니다. 대부분의 자체 호스팅 애플리케이션은 이를 지원하지 않으므로, 게이트를 통과한 모든 사용자는 애플리케이션 입장에서 동일한 단일 ID로 인식되며, 이 ID는 종종 관리자 권한을 가집니다.

또한 애플리케이션 자체 API 토큰을 보호하지 못합니다. 애플리케이션이 발급한 개인 액세스 토큰은 oauth2-proxy가 아닌 애플리케이션에 대해 인증을 수행하므로, 게이트가 그 앞에 배치되는 순간 해당 토큰은 작동을 멈춥니다. API 경로를 예외 처리하여 토큰을 다시 활성화하면, 해당 토큰이 그 경로를 보호하는 유일한 수단이 됩니다. 이제 하나의 서비스에서 두 개의 인증 시스템을 운영하게 되며, SSO는 그중 하나만 커버하게 됩니다.

세 번째 공백은 취소(revocation)입니다. 공급자(provider)에서 사용자를 삭제하면 새로운 로그인은 차단되고 cookie_refresh이 수행하는 토큰 갱신도 중단되지만, 기존 세션 쿠키는 만료될 때까지 유효합니다. cookie_expire의 기본값은 168시간이며, 이는 방금 삭제한 사용자에게 일주일간의 접근 권한을 부여하는 셈입니다. cookie_refresh을 1시간과 같이 짧게 설정하여 취소 처리가 해당 시간 내에 반영되도록 하십시오.

감사 추적(audit trail) 또한 게이트에서 멈춥니다. oauth2-proxy는 누가 언제 통과했는지 기록하지만, 애플리케이션은 이름 없는 세션으로 기록합니다. 누가 설정을 변경했는지 확인해야 한다면 애플리케이션 내의 헤더 ID가 최소한의 요건이며, 사용자별 실제 계정을 사용하는 것이 올바른 해결책입니다.

SSO 비용을 지불하는 것이 더 나은 경우

애플리케이션에 로그인 기능이 전혀 없거나 공유 비밀번호를 사용하는 경우, 그리고 사용자를 추가하거나 삭제할 중앙 관리 지점이 필요한 상황이라면 Forward auth가 적절한 도구입니다. 이 방식은 오후 시간 정도의 작업과 프로세스 하나를 추가하는 것으로 충분하며, HTTP를 지원하는 모든 애플리케이션에서 작동합니다.

하지만 동일한 애플리케이션 내에서 사용자별로 다른 권한이 필요한 경우에는 적절하지 않습니다. 게이트웨이는 "Ana는 대시보드를 수정할 수 있고 Bo는 읽기만 가능하다"와 같은 세부적인 권한을 표현할 수 없습니다. 벤더가 SSO 티어를 별도로 판매한다면, 사실상 구매하는 것은 그룹-역할 매핑 기능인 경우가 많습니다. 이를 헤더와 프록시 규칙으로 직접 구현하는 것은 비용을 지불하는 것보다 훨씬 취약합니다. 결정을 내리기 전에 SSO 티어 뒤에 숨겨진 가격 책정 패턴을 읽어보는 것이 좋습니다.

다른 두 가지 상황에서도 같은 결론이 나옵니다. 애플리케이션 내부에서 사용자별 감사 기록이 필요한 규정 준수 작업의 경우, 프록시 접근 로그를 증거로 인정하지 않습니다. 또한 브라우저 쿠키를 지원하지 않는 모바일이나 데스크톱 클라이언트를 사용하는 애플리케이션은 모든 요청마다 게이트웨이와 충돌을 일으킬 것입니다.

FAQ

포워드 인증(forward auth)이란 무엇입니까?

포워드 인증은 리버스 프록시가 업스트림으로 요청을 전달하기 전에 모든 수신 요청에 대해 별도의 인증 서비스에 확인을 요청하는 패턴입니다. 프록시는 요청 헤더를 /oauth2/auth와 같은 엔드포인트로 전송하고 상태 코드를 읽습니다. 202는 허용을 의미하므로 원래 요청이 애플리케이션으로 전달됩니다. 401은 세션이 없음을 의미하므로 프록시는 브라우저를 로그인 페이지로 리다이렉트합니다. Nginx는 auth_request 지시어로, Traefik은 forwardAuth 미들웨어로, Caddy는 forward_auth를 사용하여 이를 구현합니다.

oauth2-proxy가 로그인 페이지로 계속 리다이렉트되는 이유는 무엇입니까?

콜백 요청이 CSRF 쿠키 없이 oauth2-proxy에 도달했기 때문에 oauth2-proxy가 인증 흐름을 처음부터 다시 시작하는 것입니다. 서버 로그에는 No cookies were found in OAuth callback.이 기록되고 브라우저에는 Login Failed: Unable to find a valid CSRF token. Please try again.가 표시됩니다. 일반적인 원인은 일반 HTTP로 접속한 사이트에서 cookie_secure = true가 설정된 경우입니다. 브라우저는 http:// 출처에 Secure 쿠키를 저장하지 않기 때문입니다. 그다음으로 흔한 원인은 주소창의 호스트 이름을 포함하지 않는 cookie_domains 값 설정입니다.

API 클라이언트나 웹훅이 oauth2-proxy를 통과하게 하려면 어떻게 해야 합니까?

skip_auth_routes에 앵커(anchored) 정규 표현식을 사용하십시오. 선택적으로 특정 HTTP 메서드에만 적용할 수 있으며, 예시는 POST=^/webhook/과 같습니다. API 클라이언트가 이미 동일한 제공자가 발급한 JWT를 보유하고 있다면, skip_jwt_bearer_tokens = true을 통해 쿠키 대신 해당 토큰을 수락하고 경로를 보호할 수 있습니다. skip_auth_routes에 나열된 모든 항목은 누구에게나 인증 없이 공개되므로, 각 표현식은 호출자가 허용하는 범위 내에서 최대한 좁게 유지하십시오.

oauth2-proxy는 애플리케이션에 사용자별 권한을 제공합니까?

아니요. 이는 인증 시스템이 아니라 관문(gate) 역할을 합니다. 애플리케이션에 누가 도달할 수 있는지만 결정하며, 애플리케이션이 직접 신원 헤더를 읽어 계정에 매핑하지 않는 한 도달한 모든 사용자는 애플리케이션 입장에서 동일하게 보입니다. Grafana는 auth.proxy 설정을 통해 이를 수행할 수 있습니다. 대부분의 자체 호스팅 앱은 이를 지원하지 않으므로, 관문을 통과한 모든 사람은 앱이 실행 중인 단일 신원을 공유하게 됩니다.

누군가 신원 헤더를 직접 설정하여 oauth2-proxy를 우회할 수 있습니까?

네, 애플리케이션에 직접 접근할 수 있다면 가능합니다. 신원 정보는 X-Auth-Request-Email와 같은 일반 헤더로 전달되며, 애플리케이션은 수신한 값을 그대로 신뢰합니다. 애플리케이션 포트에 연결할 수 있는 사람은 누구나 해당 헤더를 보내어 임의의 사용자로 가장할 수 있습니다. 애플리케이션을 127.0.0.1에 바인딩하거나, 포트를 외부에 노출하지 않는 내부 Docker 네트워크에 유지하고 sudo ss -tlnp으로 이를 확인하십시오.

#oauth2-proxy#sso#oidc#reverse-proxy#forward-auth