콘텐츠로 이동

시크릿, 페어링, 디바이스 인증

암호화된 시크릿 저장소, 디바이스 페어링 및 베어러 토큰, 서비스 토큰, 그리고 민감 값 난독화에 대한 설명입니다.

Revka는 자격 증명을 평문으로 보관하지 않습니다. 설정 파일에 입력된 API 키와 토큰은 디스크에 저장되기 전에 인증 암호화로 봉인되고, 디바이스는 해시로만 보관되는 베어러 토큰으로 인증하며, 신뢰할 수 있는 로컬 사이드카는 별도의 서비스 토큰을 사용합니다. 로그에 기록되는 값은 난독화 헬퍼에 의해 잘려 출력됩니다. 이 페이지에서는 ChaCha20-Poly1305 시크릿 저장소, [secrets] 설정 키, 디바이스 페어링 가드와 베어러 토큰, 서비스 토큰, redact() 헬퍼, 그리고 레거시 enc: 포맷에서 enc2:로의 단방향 마이그레이션에 이르는 암호화 설계 전반을 설명합니다.

디스크 위의 시크릿 보호 방식을 이해하거나, 암호화 키를 복구·백업하거나, 디바이스 인증을 강화하거나, 구형 암호화 값을 마이그레이션하려는 경우 이 페이지를 참고하세요. REST 페어링 엔드포인트와 디바이스 관리 API(요청/응답 형식, 디바이스 레지스트리, 키 교체)는 페어링 및 인증을 참고하세요. 전체 레이어의 설계 근거는 보안 모델을 참고하세요.

설정에 기록하는 시크릿 — 프로바이더 API 키, 채널 토큰, 웹훅 시크릿 — 은 저장 전에 ChaCha20-Poly1305 AEAD로 암호화됩니다. 이는 심층 방어의 일환으로, 설정 파일에는 16진수로 인코딩된 암호문만 저장되어 grep, git log, 또는 우발적인 커밋으로도 원시 키가 유출되지 않습니다.

동작 방식:

  • OS CSPRNG에서 생성한 32바이트(256비트) 난수 키가 ~/.revka/.secret_key에 16진수로 저장됩니다. 값이 처음 암호화될 때 자동으로 생성됩니다.
  • 각 암호화 시 신선한 12바이트 난수 논스가 생성되므로, 동일한 평문을 두 번 암호화해도 서로 다른 암호문이 생성됩니다 — 알려진 평문 누출이 없습니다.
  • Poly1305 인증 태그로 암호문의 변조 여부를 확인할 수 있습니다. 단 1바이트라도 수정되거나 잘못된 키가 사용되면 복호화가 즉시 실패합니다.
  • 저장 값의 형식은 enc2:<hex(nonce ‖ ciphertext ‖ tag)> (12바이트 논스 + N바이트 암호문 + 16바이트 태그)입니다.

에이전트가 새 값을 자동으로 암호화하므로, 별도의 암호화 명령을 실행할 필요가 없습니다. 자격 증명을 저장하면 설정 파일의 값은 다음과 같이 표시됩니다:

[provider]
api_key = "enc2:9f3c1a...e7b204" # ChaCha20-Poly1305 ciphertext, not the real key

암호화는 기본적으로 활성화되어 있습니다. 평문 설정을 선호하는 소버린 사용자는 이를 비활성화할 수 있습니다.

[secrets]
encrypt = true # default — set false to store secrets as plaintext
타입기본값의미
secrets.encryptbooleantruetrue이면 시크릿이 enc2: 암호문으로 봉인되어 기록됩니다. false이면 값이 그대로 저장됩니다.

secrets.encrypt = false로 설정하면 암호화가 비활성화됩니다. encrypt()는 평문을 그대로 반환하고, 설정 파일에는 원시 값이 저장됩니다. 설정과 관계없이 빈 문자열은 암호화되지 않습니다.

속성
경로~/.revka/.secret_key
내용32바이트 키, 16진수 인코딩 (64자)
POSIX 권한0600, O_CREAT | O_EXCL를 통해 원자적으로 생성되어 읽기 가능한 구간이 존재하지 않음
Windows 권한takeown으로 소유권을 설정한 뒤 icacls /inheritance:r /grant:r <user>:F로 현재 사용자에게만 접근 제한

이전 Revka 빌드는 enc: 접두사를 사용하는 취약한 XOR 암호를 사용했습니다. 해당 포맷은 보안에 취약하며(알려진 평문 공격에 노출됨), 기존 설정 파일의 호환성을 위해서만 유지되고 있습니다. 현재의 안전한 포맷은 enc2: (ChaCha20-Poly1305)입니다.

마이그레이션은 읽기 시 자동으로 단방향으로 수행됩니다:

  • enc: 값은 레거시 XOR 알고리즘으로 복호화된 뒤 즉시 enc2:로 재암호화되고, 업그레이드된 값이 설정 파일에 저장됩니다. 레거시 값이 복호화될 때마다 경고가 기록됩니다.
  • enc2: 값은 이미 안전하므로 변경 없이 그대로 유지됩니다.
  • 접두사가 없는 값은 평문으로 처리되어 변경 없이 반환됩니다.
접두사알고리즘상태
enc2:ChaCha20-Poly1305 AEAD현재, 안전
enc:XOR 반복 키 암호레거시, 취약 — 읽기 시 자동 마이그레이션
(없음)평문, 그대로 반환

내부적으로 사용되며 진단 시 노출되는 감지 헬퍼는 값의 상태를 다음과 같이 분류합니다:

  • is_encrypted()enc: 또는 enc2: 중 하나이면 true.
  • is_secure_encrypted()enc2:인 경우에만 true.
  • needs_migration() — 업그레이드가 필요한 enc: 값이면 true.

디바이스 페어링 및 베어러 토큰 인증

섹션 제목: “디바이스 페어링 및 베어러 토큰 인증”

게이트웨이의 REST 페어링 흐름 외에도, Revka의 보안 레이어에는 게이트웨이와 채널의 디바이스 인증을 담당하는 **PairingGuard**가 포함되어 있습니다. 이 가드는 베어러 토큰을 발급하고, 해시로 저장하며, 페어링 단계를 무차별 대입 공격으로부터 보호합니다.

동작 방식:

  • require_pairing = true이고 기존 토큰이 없는 최초 시작 시, 6자리 일회용 페어링 코드(CSPRNG)가 터미널에 출력됩니다.
  • 클라이언트가 코드를 제출하면 256비트(32바이트) 엔트로피를 가진 베어러 토큰을 접두사와 함께 16진수로 인코딩하여 반환합니다. 평문 토큰은 딱 한 번만 표시됩니다.
  • 토큰은 SHA-256 해시로만 저장됩니다 — 가드는 평문 토큰을 절대 저장하지 않습니다. 하위 호환성을 위해 평문 토큰과 사전 해시된 (64자 16진수) 토큰 모두 로드 시 허용됩니다.
  • 페어링 코드는 타이밍 사이드채널을 방지하기 위해 상수 시간 동등 비교로 검증됩니다.
[gateway]
require_pairing = true # default — set false to disable auth (local-only use)
allow_public_bind = false # must be explicit to bind beyond localhost
타입기본값의미
gateway.require_pairingbooleantrue보호된 /api/* 라우트에 베어러 토큰 인증을 요구합니다. 부트스트랩/라이브니스 엔드포인트는 예외입니다. /api/pair는 토큰 없이 페어링 코드를 허용하고, /api/status는 기본 라이브니스 정보를 미인증 상태로 반환합니다(전체 세부 정보는 인증 시에만 제공). false로 설정하면 인증이 완전히 비활성화됩니다.
gateway.allow_public_bindbooleanfalse터널 없이 localhost 이상의 인터페이스에 바인딩하려면 명시적으로 설정해야 합니다.
gateway.paired_tokenslist(관리됨)페어링 성공 시 자동으로 기록되는 해시된 토큰입니다. 직접 수정하지 마세요.

페어링 가드는 클라이언트별로 코드 제출 실패 횟수를 추적합니다. 너무 많은 실패가 발생하면 해당 클라이언트가 잠금됩니다:

상수의미
MAX_PAIR_ATTEMPTS5잠금 전 허용되는 코드 입력 실패 횟수
PAIR_LOCKOUT_SECS300잠금 지속 시간 (5분)

잠금은 클라이언트 단위이며 전역이 아니므로, 한 명의 공격자가 모든 사용자를 잠글 수 없습니다. 임계값 이전에 올바른 코드를 제출하면 정상적으로 페어링됩니다. (게이트웨이는 REST 엔드포인트에 이 위에 IP 기반 AuthRateLimiter 레이어를 추가로 적용합니다 — 페어링 및 인증을 참고하세요.)

  1. 게이트웨이를 시작합니다. 터미널에 일회용 페어링 코드가 출력됩니다.

    Terminal window
    revka gateway
    # Pairing code: 123456
  2. 코드를 토큰으로 교환합니다 (전체 요청 및 응답 형식은 페어링 API를 참고하세요).

    Terminal window
    curl -X POST http://127.0.0.1:42617/api/pair \
    -H 'Content-Type: application/json' \
    -d '{"code": "123456", "device_name": "My Laptop", "device_type": "cli"}'
  3. 반환된 토큰을 저장합니다 — 단 한 번만 표시됩니다. 가드는 SHA-256 해시만 저장합니다.

  4. 이후의 모든 요청을 토큰으로 인증합니다.

    Terminal window
    curl http://127.0.0.1:42617/api/status \
    -H "Authorization: Bearer <token>"

코드는 최초 사용 시 소진됩니다 — 코드당 한 번만 페어링하고, 이후에는 새 코드로 교체하세요. 토큰은 성공 시 설정 파일에 저장되므로, 데몬 재시작 후에도 다시 페어링할 필요가 없습니다.

신뢰할 수 있는 로컬 사이드카 — 주로 operator-mcp 런타임 — 는 사용자 베어러 토큰 대신 서비스 토큰으로 머신 간 인증을 수행합니다. 전용 헤더로 전송됩니다:

X-Revka-Service-Token: <service-token>

이 헤더는 권한이 필요한 로컬 접근 라우트에서 Authorization: Bearer와 함께 허용됩니다. 서비스 토큰은 게이트웨이 시작 시 자동 생성되며, 사용자 베어러 토큰보다 더 높은 권한을 가진 별도의 자격 증명입니다. 저장된 워크플로 자격 증명을 복호화하고 비용 원장을 기록하는 경로이며, 워크스페이스 에셋 URL 서명에도 동일한 키가 사용됩니다.

시크릿이 로그에 유출되지 않도록, Revka는 코드베이스 전반에 걸쳐 모듈 수준의 redact() 헬퍼를 사용합니다. 이 헬퍼는 멀티바이트 UTF-8에서도 패닉 없이 문자 경계를 안전하게 처리하여 앞 4자를 보여주고 나머지를 ***로 대체합니다:

redact("sk-ant-abcdef...") → "sk-a***"
redact("abcd") → "***" // 4 or fewer chars → fully masked

이는 의도적으로 단순하게 설계된 기능입니다 — 로그 라인이 어떤 자격 증명을 참조하는지 식별할 수 있을 정도로만 노출합니다. zeroize-on-drop 기본 요소가 아니므로 메모리에서 시크릿을 지우지 않습니다. 완전한 보호를 위해서는 암호화 시크릿 저장소 및 아웃바운드 누출 탐지와 함께 사용하세요. redact()는 민감한 값이 로깅되는 지점에서 명시적으로 호출해야 하는 수동 헬퍼입니다. 옵트인한 호출 지점에서만 적용되며 — 자동 로깅 인터셉터가 없으므로, 코드가 redact()를 통해 값을 처리하는 경우에만 난독화됩니다.

  1. secrets.encrypt = true를 유지하여 모든 자격 증명이 enc2: 암호문으로 저장되도록 합니다.

  2. ~/.revka/.secret_key를 안전한 위치에 백업합니다 — 이 파일이 없으면 암호화된 시크릿은 복구할 수 없습니다.

  3. require_pairing = true를 유지하고, require_pairing = falseallow_public_bind = true를 절대 함께 사용하지 마세요.

  4. 레거시 값을 마이그레이션합니다 — 원래 키가 있는 상태로 에이전트를 한 번 로드하여 enc: 값이 enc2:로 자동 업그레이드되도록 합니다.

  5. 서비스 토큰을 보호합니다 — 신뢰할 수 있는 로컬 사이드카에만 제한하고, 브라우저에 절대 노출하지 마세요.