콘텐츠로 이동

TLS, 속도 제한, WebAuthn, 정적 서빙

인증서 고정을 포함한 애플리케이션 수준 TLS/mTLS, 속도 제한, WebAuthn, 대시보드 정적 서빙, HMAC 서명 워크스페이스 에셋, 경로 프리픽스, 클릭 추적.

이 페이지에서는 게이트웨이의 전송 보안 및 서빙 관련 기능을 다룹니다. 인증서 고정을 포함한 애플리케이션 수준 TLS 및 상호 TLS, 게이트웨이 속도 제한기와 웹훅 멱등성, 선택적 WebAuthn 하드웨어 키 인증, 그리고 게이트웨이가 제공하는 네 가지 파일 서빙 경로(대시보드 SPA /_app/*, HMAC 서명 워크스페이스 에셋 /workspace/*, 클릭 추적 리다이렉트 /track/c/*, Claude Code 훅 엔드포인트)를 설명합니다. 게이트웨이를 공용 인터넷에 배포하거나 리버스 프록시 뒤에 두거나, 신뢰할 수 있는 서비스에 클라이언트 인증서를 배포하거나, 대시보드 빌드를 배포 파이프라인에 연결할 때 이 페이지를 참고하세요.

이어지는 내용의 대부분은 인증된 엔드포인트보다는 설정에 관한 것입니다. Bearer 토큰 인증 및 페어링 흐름은 페어링 및 인증에서 별도로 다루며, 토큰과 시크릿의 암호화 설계는 시크릿, 페어링 및 디바이스 인증에서 확인할 수 있습니다. 전체 라우트 맵은 게이트웨이 API 개요를 참조하세요.

[gateway.tls] enabled = true로 설정하면 게이트웨이는 rustls를 사용해 애플리케이션 수준에서 직접 TLS를 종료합니다. axum의 일반 serve 대신 원시 TCP accept 루프를 사용하는 방식입니다. 이는 리버스 프록시(nginx, Caddy)에서 TLS를 종료하는 방식의 대안입니다. 게이트웨이 앞에 프록시가 없거나 프로세스 수준까지 상호 TLS가 필요한 경우 애플리케이션 수준 TLS를 사용하세요.

[gateway.tls]
enabled = true
cert_path = "/etc/revka/server.pem" # PEM-encoded server certificate
key_path = "/etc/revka/server.key" # PEM-encoded private key
타입기본값설명
gateway.tls.enabledboolfalse애플리케이션 수준 TLS 활성화
gateway.tls.cert_pathstringPEM 서버 인증서 경로
gateway.tls.key_pathstringPEM 개인 키 경로

[gateway.tls.client_auth] 섹션을 추가하면 연결하는 클라이언트가 운영자가 제어하는 CA가 서명한 인증서를 제시하도록 요구할 수 있습니다. 이 기능은 클라이언트 인증서를 신뢰할 수 있는 서비스 집합에 배포하는 운영 환경을 위한 것입니다.

[gateway.tls.client_auth]
enabled = true
ca_cert_path = "/etc/revka/ca.pem" # CA that signs accepted client certs
require_client_cert = true
타입기본값설명
client_auth.enabledboolfalse클라이언트 인증서 검증(mTLS) 활성화
client_auth.ca_cert_pathstring클라이언트 인증서 검증에 사용할 CA 인증서
client_auth.require_client_certbooltrue유효한 클라이언트 인증서가 없는 연결을 거부합니다. 선택적 클라이언트 인증을 허용하려면 false로 설정하세요
client_auth.pinned_certsstring[][]인증서 고정을 위한 SHA-256 지문 목록

pinned_certs가 비어 있지 않으면 검증기는 먼저 일반 CA 유효성 검사를 수행한 뒤, 제시된 클라이언트 인증서의 SHA-256 지문이 고정된 값 중 하나와 일치하는지를 추가로 확인합니다. 지문은 비교 전에 정규화됩니다(콜론 제거 및 소문자 변환). 따라서 두 형식 모두 허용됩니다.

[gateway.tls.client_auth]
enabled = true
ca_cert_path = "/etc/revka/ca.pem"
pinned_certs = [
"aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899",
"AA:BB:CC:DD:EE:FF:...",
]

OpenSSL로 고정할 지문을 계산하려면:

Terminal window
openssl x509 -fingerprint -sha256 -noout -in client.pem

게이트웨이는 클라이언트 IP별로 키가 지정된 두 개의 독립적인 슬라이딩 윈도우 속도 제한기와 웹훅용 인-메모리 멱등성 저장소를 운영합니다. 인증 속도 제한기는 페어링/인증 엔드포인트에 특화된 무차별 대입 공격 방어 장치입니다(페어링 및 인증 참조). 여기서 설명하는 게이트웨이 수준 속도 제한기는 페어링 및 웹훅 라우트에 적용되는 일반적인 분당 제한입니다.

[gateway] 섹션에서 조정할 수 있습니다.

[gateway]
pair_rate_limit_per_minute = 10 # 0 disables the pairing rate limit
webhook_rate_limit_per_minute = 60 # 0 disables the webhook rate limit
rate_limit_max_keys = 10000 # max tracked IP keys (LRU eviction)
idempotency_ttl_secs = 300 # webhook idempotency window
idempotency_max_keys = 10000 # max idempotency keys
trust_forwarded_headers = false # see caution below
타입기본값설명
gateway.pair_rate_limit_per_minuteinteger10IP당 분당 페어링 시도 횟수. 0으로 비활성화.
gateway.webhook_rate_limit_per_minuteinteger60IP당 분당 웹훅 요청 횟수. 0으로 비활성화.
gateway.rate_limit_max_keysinteger10000LRU 제거 전 최대 추적 IP 키 수
gateway.idempotency_ttl_secsinteger300웹훅 멱등성 키가 재전송을 중복 제거하는 시간
gateway.idempotency_max_keysinteger10000최대 추적 멱등성 키 수
gateway.trust_forwarded_headersbooleanfalse클라이언트 키에 X-Forwarded-For / X-Real-IP 사용

별도로 설정할 수 없는 내장 기본값: 속도 제한 윈도우는 60초, 요청 본문 상한은 65,536바이트(64 KiB), 기본 요청 타임아웃은 30초입니다(환경 변수 REVKA_GATEWAY_TIMEOUT_SECS로 재정의 가능 — 웹 검색이나 서브 에이전트 위임을 포함한 에이전틱 턴은 30초를 초과하는 경우가 많으므로 유용합니다). 만료된 속도 제한기 항목은 5분마다 정리되어 메모리 사용량이 제한됩니다.

일반 POST /webhook 엔드포인트는 X-Idempotency-Key 헤더를 기준으로 재전송된 요청을 중복 제거합니다. idempotency_ttl_secs 이내에 동일한 키가 다시 들어오면 에이전트를 재실행하지 않고 200을 반환합니다. 웹훅 전체 계약은 웹훅 수신을 참조하세요.

기본적으로 속도 제한기와 잠금 키는 전달된 헤더가 아닌 TCP 소켓 피어 주소를 기준으로 합니다. trust_forwarded_headers = true로 설정하면 게이트웨이는 X-Forwarded-For / X-Real-IP의 가장 오른쪽 홉에서 클라이언트 키를 가져옵니다. 이는 모든 트래픽을 단일 출발지 IP로 재작성하는 리버스 프록시 뒤에 있을 때 올바른 동작입니다.

WebAuthn / FIDO2를 사용하면 하드웨어 보안 키(YubiKey, SoloKey)나 플랫폼 인증기(Touch ID, Windows Hello)로 게이트웨이에 인증할 수 있습니다. Bearer 토큰의 대안이거나 추가 인증 수단으로 활용할 수 있습니다. 이 기능은 설정 플래그와 컴파일 타임 기능 플래그 모두 필요합니다.

WebAuthn은 기본 빌드에 포함되어 있지 않습니다. webauthn Cargo 기능을 지정해 빌드한 뒤 설정에서 활성화해야 합니다.

Terminal window
cargo build --release --features webauthn
[security.webauthn]
enabled = true
rp_id = "revka.example.com" # Relying Party ID (a domain name)
rp_origin = "https://revka.example.com" # Relying Party origin URL
rp_name = "Revka" # display name shown by the authenticator
타입기본값설명
security.webauthn.enabledboolfalseWebAuthn 등록 및 인증 활성화
security.webauthn.rp_idstring"localhost"Relying Party ID — 대시보드를 서빙하는 도메인과 일치해야 함
security.webauthn.rp_originstring"http://localhost:42617"Relying Party 오리진 URL
security.webauthn.rp_namestring"Revka"Relying Party 표시 이름

모든 WebAuthn 라우트는 Bearer 토큰을 요구합니다. 등록과 인증은 2단계로 이루어집니다. start 호출은 브라우저가 인증기에 전달할 챌린지 옵션을 반환하고, finish 호출은 인증기의 응답을 제출합니다.

메서드경로용도
POST/api/webauthn/register/start등록 시작 — PublicKeyCredentialCreationOptions 반환
POST/api/webauthn/register/finish인증기 응답으로 등록 완료
POST/api/webauthn/auth/start인증 시작 — PublicKeyCredentialRequestOptions 반환
POST/api/webauthn/auth/finish인증기 어설션으로 인증 완료
GET/api/webauthn/credentials?user_id=...사용자의 등록된 자격증명 목록 조회
DELETE/api/webauthn/credentials/{id}?user_id=...등록된 자격증명 삭제
POST /api/webauthn/register/start
Authorization: Bearer rk_<token>
Content-Type: application/json
{ "user_id": "alice", "user_name": "Alice" }

인증은 사용자 정보만으로 시작합니다.

{ "user_id": "alice" }

finish 본문은 챌린지와 인증기의 RegisterCredentialResponse / AuthenticateCredentialResponse를 포함합니다. 자격증명 목록은 credential_id, label, registered_at, sign_count를 반환합니다.

정적 대시보드 파일 서빙 (/_app/*)

섹션 제목: “정적 대시보드 파일 서빙 (/_app/*)”

게이트웨이는 웹 대시보드 단일 페이지 앱(SPA)을 서빙합니다. 컴파일된 에셋은 /_app/*에서 제공되며, API와 일치하지 않는 GET 요청은 index.html로 폴백됩니다(SPA 라우팅).

라우트용도
GET /_app/{*path}정적 대시보드 에셋(JS, CSS, 이미지)
GET / 및 SPA 폴백클라이언트 사이드 라우팅을 위한 index.html 서빙

게이트웨이는 파일시스템 경로를 우선하고, 없으면 빌드 시 바이너리에 내장된 번들로 폴백합니다.

  1. REVKA_WEB_ROOT 환경 변수(설정된 경우)
  2. gateway.web_root 설정 키(설정된 경우)
  3. 바이너리에 컴파일된 내장 web/dist 번들
[gateway]
web_root = "/srv/revka/web/dist" # optional; serve the dashboard from disk
소스키 / 변수
환경 변수REVKA_WEB_ROOT
설정gateway.web_root

에셋 응답은 assets/ 하위 파일에 대해 적극적으로 캐시됩니다(Cache-Control: public, max-age=31536000, immutable). index.html은 배포 즉시 반영되도록 no-cache로 서빙됩니다. 경로 처리는 .. 트래버설, 역슬래시 구분자, 절대 경로, 심볼릭 링크 탈출을 모두 거부합니다. 정규화된 경로는 웹 루트 내에 있어야 합니다.

경로 프리픽스 (리버스 프록시 하위 경로 배포)

섹션 제목: “경로 프리픽스 (리버스 프록시 하위 경로 배포)”

게이트웨이를 하위 경로(예: https://host/revka/) 아래에서 서빙하려면 gateway.path_prefix를 설정하세요. 비어 있지 않은 프리픽스가 설정되면 SPA 폴백이 index.html 내의 /_app/ 참조를 재작성하고 window.__REVKA_BASE__를 주입해, 프런트엔드가 에셋 및 API URL을 올바르게 해석할 수 있게 합니다.

[gateway]
path_prefix = "/revka" # must start with "/" and must NOT end with "/"

path_prefix는 시작 시 유효성이 검사됩니다. /로 시작해야 하고, /로 끝나면 안 되며(단독 /도 거부), URL 예약되지 않은 문자와 하위 구분자 문자만 포함할 수 있습니다. 잘못된 값은 자동으로 잘리지 않고 유효성 검사 실패로 처리됩니다. 전체 리버스 프록시 설정은 네트워크 배포, Raspberry Pi 및 프록시를 참조하세요.

워크스페이스 에셋 서빙 (HMAC 서명)

섹션 제목: “워크스페이스 에셋 서빙 (HMAC 서명)”

config.workspace_dir 아래의 생성된 이미지, 에이전트 출력, 기타 아티팩트는 GET /workspace/{*path}에서 제공됩니다. 브라우저는 서브리소스 fetch(예: <img> 로드)에 Authorization 헤더를 첨부하지 않으므로, 이 URL은 Bearer 토큰 대신 HMAC-SHA256 서명과 만료 시간을 쿼리 문자열로 사용해 인증합니다.

GET /workspace/<rel-path>?exp=<unix-ts>&sig=<hex-sha256-hmac>
파라미터설명
expUnix 타임스탬프 만료 시간. 이 시간 이후 요청은 403 반환
sigrel_path + "\n" + exp에 대한 16진수 HMAC-SHA256

서명 키는 게이트웨이의 서비스 토큰(operator-mcp와 공유하는 동일한 키, ~/.revka/service-token에서 읽음)이므로, 도구는 게이트웨이를 거치지 않고 서명된 URL을 직접 생성할 수 있습니다. 기본 유효 기간은 3600초입니다. 서명된 URL은 상대 경로(/workspace/...)로 반환되므로, 대시보드를 로컬에서 접근하든 터널을 통해 접근하든 동일한 링크가 정상 동작합니다. 응답은 파일 확장자로 추정된 MIME 타입과 함께 Cache-Control: private, max-age=300을 설정합니다.

경로 처리는 절대 경로, .. 트래버설, 심볼릭 링크 탈출을 거부합니다. 정규화된 경로는 workspace_dir 아래에 있어야 합니다. HMAC은 경로 조작을 방지하고, 만료 시간은 URL이 장기간 유출되는 것을 막습니다. 이 경로는 에이전트가 생성한 캔버스/HTML 콘텐츠 및 아바타/에셋 URL을 지원합니다. 실시간: WebSocket, SSE & Live Canvas를 참조하세요.

워크플로우 email: 단계의 track_clicks 기능은 아웃바운드 링크를 게이트웨이의 리다이렉트 핸들러를 통과하도록 재작성합니다. 핸들러는 클릭을 기록한 뒤 원래 목적지로 302 리다이렉트합니다.

GET /track/c/<encoded_kref>?u=<url-encoded-destination>

이 엔드포인트는 설계상 인증이 없습니다. 콜드 이메일 수신자는 Bearer 토큰이 없기 때문입니다. 경로의 토큰은 추적된 kref를 인코딩합니다.

  • 서명 없음: 패딩이 제거된 urlsafe_base64(kref).
  • 서명 있음: urlsafe_base64(kref + ":" + hmac_sha256(secret, kref)[:8]) — 8바이트 HMAC으로 조작을 감지하면서 URL을 짧게 유지합니다. CLICK_TRACKING_SECRET 환경 변수를 설정하면 서명이 활성화되며, 시크릿이 설정되고 HMAC이 일치할 때만 리다이렉트가 verified로 표시됩니다.
Terminal window
export CLICK_TRACKING_SECRET="<your-shared-secret>"

핸들러는 의도적으로 빠르게 설계되었습니다. 이메일 클라이언트(특히 Gmail)는 링크를 미리 가져오고 느린 리다이렉트는 포기하므로 200ms 이내에 응답해야 합니다. 클릭은 리다이렉트 전 Kumiho나 데이터베이스 쓰기 없이 핫 패스의 tracing을 통해 기록됩니다. ?u= 파라미터가 없으면 400을 반환하지만, 잘못된 토큰이더라도 리다이렉트는 수행됩니다(로깅은 최선 노력이며 리다이렉트를 희생하지 않습니다).

게이트웨이는 ClaudeCodeRunnerTool에 의해 실행된 Claude Code 서브프로세스의 수명 주기 훅 수신기를 제공합니다. 이 서브프로세스는 페어링 토큰을 얻을 수 없으므로 엔드포인트는 Bearer 인증이 필요하지 않습니다.

POST /hooks/claude-code
Content-Type: application/json
{
"session_id": "...",
"event_type": "...",
"tool_name": "...",
"summary": "..."
}
필드설명
session_id이벤트가 속한 Claude Code 세션
event_type수명 주기 이벤트 타입(도구 실행, 완료, 오류)
tool_name이벤트와 관련된 도구
summary사람이 읽을 수 있는 이벤트 요약

핸들러는 현재 이벤트를 기록하고 {"ok": true}를 반환합니다.

메서드 및 경로인증용도
POST /api/webauthn/register/startbearerWebAuthn 등록 시작
POST /api/webauthn/register/finishbearerWebAuthn 등록 완료
POST /api/webauthn/auth/startbearerWebAuthn 인증 시작
POST /api/webauthn/auth/finishbearerWebAuthn 인증 완료
GET /api/webauthn/credentialsbearer등록된 자격증명 목록
DELETE /api/webauthn/credentials/{id}bearer자격증명 삭제
GET /_app/{*path}없음대시보드 정적 에셋
GET /workspace/{*path}HMAC 서명서명된 워크스페이스 에셋 서빙
GET /track/c/{encoded}없음클릭 추적 리다이렉트
POST /hooks/claude-code없음Claude Code 수명 주기 훅 수신기
설정기본값기능
[gateway.tls] enabledfalse애플리케이션 수준 TLS
[gateway.tls.client_auth] enabledfalse상호 TLS
[gateway.tls.client_auth] pinned_certs[]인증서 고정
gateway.pair_rate_limit_per_minute10페어링 속도 제한
gateway.webhook_rate_limit_per_minute60웹훅 속도 제한
gateway.trust_forwarded_headersfalse프록시 인식 클라이언트 키잉
[security.webauthn] enabledfalseWebAuthn(webauthn 기능 필요)
gateway.web_root / REVKA_WEB_ROOT내장 번들대시보드 소스
gateway.path_prefix미설정하위 경로 배포
CLICK_TRACKING_SECRET (env)미설정클릭 추적 HMAC
REVKA_GATEWAY_TIMEOUT_SECS (env)30요청 타임아웃 재정의