AI 에이전트를 붙여 놓고 일을 시키다 보면 이런 순간이 옵니다. "그 API 키 좀 알려주세요." 에이전트가 외부 서비스를 호출해야 하는데 자격 증명이 없는 거예요. 저는 처음에 그냥 채팅창에 붙여넣었습니다. 그러면 그 값이 대화 기록에도, 세션 로그에도, 모델 컨텍스트에도 남습니다. 지우기도 번거롭고 지웠다고 안심하기도 어렵죠.
OpenClaw에는 이 상황 전용 도구가 있습니다. secrets입니다. 에이전트가 자격 증명을 달라고 요청은 하지만 그 값은 보지 못하는 구조예요. 이번 글은 이 도구와 그 뒤에 있는 SecretRef·센티넬 구조를 공식 문서 기준으로 정리한 것입니다. 출처는 OpenClaw 공식 문서 tools/secrets.md, gateway/secrets.md와 그 하위 페이지 secrets/runtime-model, secrets/secret-store-and-egress입니다(docs.openclaw.ai). 제가 직접 침해를 겪었거나 성능을 측정한 내용은 아닙니다.
값을 안 보고 받는다는 게 무슨 뜻인가
흐름은 세 단계입니다. 에이전트가 항목 이름을 정해서 요청합니다. 사람이 마스킹된 입력창에 값을 넣습니다. 게이트웨이가 그 값을 곧장 공유 시크릿 저장소에 씁니다.
에이전트가 받는 건 값이 아니라 항목 메타데이터와 저장소 SecretRef뿐입니다. 문서는 그 값이 채팅, 세션 기록, 도구 결과, 모델 컨텍스트에 나타나지 않는다고 적어 놓았습니다. 중간에 에이전트를 거치지 않는다는 게 이 도구의 전부예요.
쓸 수 있는 세션에 제한이 있습니다. 주 에이전트 세션에서는 쓸 수 있지만 서브에이전트와 ACP 워커 세션에는 이 도구가 주어지지 않습니다. 서브에이전트에게 일을 쪼개 맡기는 구조를 쓰신다면 기억해 둘 만합니다. 서브에이전트 권한이 어떻게 정해지는지는 서브에이전트 글에 따로 정리해 뒀어요.
기본값은 켜짐입니다. 전용 설정 키는 없고 일반 도구 정책을 그대로 따릅니다. 빼고 싶으면 다른 도구와 똑같이 거부하면 됩니다. openclaw.json에 tools.deny: ["secrets"]를 넣는 식이죠. 허용 목록과 도구 프로필도 같은 방식으로 적용됩니다. 도구가 막히는 층이 여러 개라서 헷갈리시면 도구 정책과 Elevated 구분 글을 먼저 보셔도 좋습니다.
동작은 셋뿐입니다
request는 사람에게 자격 증명을 물어서 이름을 붙여 저장합니다. STRIPE_API_KEY 같은 이름이요. 여기에 중요한 제한이 하나 있습니다. 요청은 보호된 시크릿만 가능해요. env 종류는 list로 값이 읽히기 때문에, 그걸 요청하게 두면 마스킹된 입력창이 하는 약속이 깨지거든요.
요청에는 에이전트가 allowedHosts 제안과 짧은 reason을 붙일 수 있고, 그 이유는 승인 화면에 그대로 보입니다. 요청은 사람이 답하거나 건너뛰거나 시간이 다 될 때까지 막혀 있습니다. 기본 대기는 15분이고 timeoutSeconds는 30초에서 3600초 사이로 조정됩니다. 이건 사람을 기다리는 최대 시간이고, 더 먼저 취소되거나 전체 실행 제한에 걸리면 그때 끝납니다. 요청한다고 실행 예산이 늘어나지는 않아요.
요청은 요청한 에이전트 실행에 묶여 있습니다. 답하기 전에 그 권한이 닫히면 대기 중인 화면은 취소되고 저장도 거부됩니다. 밤에 돌려 둔 예약 작업이 조용히 아무것도 저장하지 못한 채 끝나는 경우가 여기에 해당하겠죠.
list는 메타데이터만 돌려줍니다. 이름, 종류, 허용 호스트, 마지막 갱신 시점이요. 문서 표현으로는 시크릿 값이 목록에서 구조적으로 없습니다. 빈 문자열로 가려 놓은 게 아니라 애초에 필드가 없다는 뜻입니다.
delete는 이름으로 지우는데 소프트 삭제입니다. 지운 항목은 30일 뒤에 완전히 사라집니다.
그리고 없는 동작이 하나 있습니다. 에이전트가 값을 직접 써 넣는 동작은 일부러 만들지 않았습니다. 값이 저장소에 들어가는 길은 셋뿐이에요. 사람이 답하는 승인 화면, Control UI의 /settings/secrets 페이지, 그리고 openclaw secrets store 명령입니다.
env 항목은 예외예요
저장소 항목은 접근 방식이 두 가지입니다. 이 구분을 놓치면 안전하다고 착각하기 쉬워요.
보호된 시크릿(kind: "secret")은 저장한 뒤로는 쓰기 전용입니다. 게이트웨이 목록, Control UI, CLI 조회 어디에도 값이 나오지 않고 값을 다시 보여 주는 RPC도 없습니다. 이 값은 설정 필드가 SecretRef로 참조하거나, 목적지가 묶인 시크릿 이그레스 프록시가 쓸 때까지 아무 일도 하지 않습니다.
에이전트가 읽을 수 있는 환경값(kind: "env")은 반대입니다. 관리자에게 Control UI에서 보이고 store list·store get으로도 돌아옵니다. 게이트웨이가 호스팅하는 exec 명령에는 평문으로 들어갑니다. 문서는 여기서 아주 분명하게 적어 놓았습니다. 에이전트가 이 값을 출력하고 전송하고 저장할 수 있습니다.
그러니까 같은 저장소라도 env에 넣은 순간 그건 비밀이 아니라 설정값입니다. API 키를 편하게 쓰려고 env로 넣어 두면 보호 장치를 스스로 끈 겁니다.
참고로 env 평문은 모든 실행 경로에 가지도 않습니다. Codex 네이티브 셸, Codex 샌드박스 exec 서버, Claude Code 같은 ACP 자식 프로세스, OpenClaw 샌드박스 exec, 원격 노드 exec에는 들어가지 않습니다. 이 경로들은 자식 환경을 다르게 조립하거든요. 원격 노드에서 권한이 어떻게 열리는지는 노드 글에 정리해 뒀습니다.
요청 화면은 어디로 오나
웹 Control UI에서는 입력창 위에 마스킹된 칸이 붙습니다. 화면에는 누가 요청했는지(에이전트와 세션), 항목 이름, 에이전트가 적은 이유, 그리고 편집 가능한 허용 호스트 목록이 나옵니다. 그 자격 증명이 어디로 나갈 수 있는지는 사람이 마지막에 정한다는 뜻이에요.
이미 있는 이름이면 그 사실과 함께 언제 누가 마지막으로 바꿨는지도 보여 줍니다. 그대로 제출하면 값이 교체됩니다. 게이트웨이는 제출한 값을 앞뒤 공백까지 그대로 보존합니다. 웹·TUI·애플 앱의 입력칸은 한 줄이라, PEM 키처럼 여러 줄짜리는 CLI의 --value-file로 넣어야 합니다.
TUI도 마스킹 입력을 받습니다. 타이핑한 글자는 점으로 보이고 채팅 로그나 입력 기록에 남지 않습니다. 다만 TUI에서는 호스트 목록이 읽기 전용이에요. 제출하면 화면에 보이는 목록을 그대로 받는 거고, 고치려면 Control UI를 써야 합니다. Skip은 거절이고 Esc는 대기 상태로 두는 것이라 /question으로 다시 열 수 있습니다.
그리고 채팅 채널 얘기입니다. 텔레그램이나 디스코드는 값을 아예 받지 않습니다. 이런 곳에는 Control UI 승인 화면으로 가는 링크만 전달됩니다. 채팅 메시지로 자격 증명을 적어 보내도 답으로 잡히지 않아요. 애초에 그걸 피하려고 만든 흐름이니 당연한 설계입니다. 대신 링크에는 조건이 있습니다. Control UI가 켜져 있어야 하고 gateway.publicOrigin이 설정돼 있어야 합니다. 쓸 만한 링크를 만들 수 없으면 전달이 눈에 보이는 막힘을 보고하고 대기 중인 요청을 취소합니다.
iOS·macOS·안드로이드 앱은 같은 카드를 마스킹된 칸으로 보여 줍니다. Control UI, TUI, 네이티브 앱 카드는 이미 맺어진 게이트웨이 연결로 오기 때문에 gateway.publicOrigin이나 공개 링크가 필요하지 않습니다. 링크가 필요한 건 채팅 채널 쪽이죠. 답장이 어느 채널로 돌아가는지 헷갈리시면 채널 라우팅 글이 도움이 됩니다.
권한도 양쪽이 다릅니다. 요청을 만드는 쪽은 operator.admin에 더해 에이전트의 신뢰된 실시간 실행 권한이 있어야 합니다. 실행 ID만 있거나 관리자 연결만 있는 것으로는 안 됩니다. 반면 답하는 쪽은 평범한 질문 권한과 그 세션 접근이면 됩니다. 답하는 건 값을 읽는 게 아니라 값을 주는 일이니까요.
로컬 모드에서는 저장이 안 됩니다
openclaw chat이나 openclaw tui --local로 띄운 로컬 모드는 저장소에 묶인 요청을 처리할 수 없습니다. 에이전트는 막힘 안내를 받고, 운영자에게 openclaw secrets store를 쓰거나 게이트웨이를 띄운 뒤 Control UI를 쓰라고 안내하게 됩니다.
로컬 질문 화면 자체는 마스킹 입력을 지원합니다. 그것만으로는 안 되는 거예요. 게이트웨이가 하는 저장소 쓰기와 런타임 갱신 흐름이 빠져 있어서입니다.
허용 호스트가 정하는 것
여기가 제일 오해하기 쉬운 부분이라고 느꼈습니다. 허용 호스트는 게이트웨이 이그레스 치환을 지배합니다. 설정 SecretRef를 지배하지는 않습니다.
무슨 말이냐면, 호스트 목록을 비워 두면 이그레스 치환이 막힙니다. 그래도 지원되는 설정 필드는 저장된 자격 증명을 계속 읽어 옵니다. 호스트를 비웠으니 이 키는 이제 아무 데도 못 나간다고 생각하면 틀립니다.
이그레스 자체에도 조건이 붙습니다. 프록시가 켜져 있어야 합니다. 이 프록시는 기본값이 꺼짐이에요.
openclaw secrets store set OPENAI_API_KEY --allow-host api.openai.com
openclaw config set secrets.egressProxy.enabled true --strict-json
openclaw gateway restart
호스트 매칭 규칙은 느슨한 구석이 없습니다. 소문자 ASCII 또는 퓨니코드로 저장되고 정확히 일치해야 합니다. 와일드카드, 접미사 매칭, 포트는 지원하지 않습니다. 허용 호스트가 없는 시크릿은 절대 치환되지 않습니다. 묶이지 않은 호스트로 나가는 요청은 거부되면서 어떤 시크릿인지와 실행해야 할 store set ... --allow-host ... 명령까지 그대로 알려 줍니다.
기본값 처리도 알아 둘 만합니다. 에이전트가 호스트 제안을 생략하면 기존 항목을 교체할 때는 그 항목의 현재 호스트로 시작합니다. 새 항목이면 빈 목록으로 시작합니다. 명시적으로 빈 제안을 보냈으면 빈 채로 갑니다.
그리고 문서에 못 박아 둔 경고가 있습니다. 프록시가 없거나 목적지 허가가 없다고 해서, 명령·인자·URL·로그·채팅에 평문을 넣는 방식으로 우회하지 말라는 겁니다.
저장한 값을 실제로 쓰는 길
저장이 끝나면 도구 결과가 status: "stored"와 SecretRef, 그리고 currentPolicy를 돌려줍니다. 여기서 currentPolicy의 성질을 문서가 따로 설명해 놓았어요. 이건 후속 메타데이터 조회 한 번으로 읽은 항목의 현재 호스트 목록입니다. 게이트웨이 설정도 아니고 변경 불가능한 승인 영수증도 아닙니다. 다른 쓰기가 그 사이에 호스트를 바꿨을 수도 있거든요.
그래서 에이전트는 자기가 제안한 호스트가 아니라 현재 호스트를 보고해야 합니다. 목록 전체를 확인할 수 없으면 호스트에 대해 아무 주장도 하지 않아야 합니다. 직렬화한 목록이 512자를 넘으면 omitted로 개수만 돌아오기 때문에 이런 상황이 실제로 생깁니다.
돌려받은 ref는 SecretRef를 받는 자리에 그대로 쓸 수 있습니다. 모델 제공자 API 키나 채널 토큰 같은 곳이죠. 기본 저장소 별칭이면 모양이 이렇습니다.
{ "source": "store", "provider": "default", "id": "STRIPE_API_KEY" }
쓰기가 일어나면 영향받는 설정과 인증 프로필 참조가 갱신됩니다. 갱신이 성공하면 준비돼 있던 모델 상태도 교체돼서, 이후 카탈로그 조회는 새 자격 증명을 씁니다.
저장이 커밋되고 나면 그 요청은 답이 끝난 상태입니다. 다시 제출할 수 없습니다. 이후 런타임 갱신이 실패해도 저장은 되돌아가지 않아요. 그럴 때는 다시 답을 넣는 게 아니라 보고된 제공자 오류를 고치고 openclaw secrets reload를 돌리는 겁니다.
한 가지 편한 성질도 있습니다. 게이트웨이 세션에서는 관계없는 제공자의 자격 증명이 없다고 해서 정상 제공자로 돌아가는 턴이 막히지 않습니다. 그 정상 모델을 써서 빠진 항목을 요청할 수 있어요. 물론 문제 있는 제공자를 직접 고르면 SecretRef가 풀릴 때까지 계속 실패합니다. OpenClaw는 환경 변수나 인증 프로필 자격 증명을 조용히 대신 끼워 넣지 않습니다. 모델이 왜 다른 걸로 넘어가는지는 모델 폴백 글에 정리해 뒀습니다.
센티넬이 막아주는 것과 못 막는 것
SecretRef로 받은 모델 제공자 자격 증명에는 OpenClaw가 프로세스 안에서만 통하는 불투명한 센티넬을 만들어 씁니다. 그래서 인증 저장소, 스트림 옵션, SDK 설정, 로그, 오류 객체, 웬만한 런타임 조회는 실제 키가 아니라 oc-sent-v2.<암호문>.end 같은 값을 보게 됩니다. 요청이 프로세스를 떠나기 직전에 URL과 헤더의 센티넬이 실제 값으로 바뀝니다.
모르는 센티넬 모양 값은 네트워크 동작 전에 실패로 처리됩니다. 풀리지 않은 센티넬을 제공자에게 그냥 보내는 대신 요청을 거부한다는 거예요. 확인된 시크릿 값은 로그에서 정확히 일치하는 문자열로 가려지도록 따로 등록도 됩니다.
여기서 문서가 스스로 한계를 적어 둡니다. 센티넬은 프로세스 격리가 아닙니다. 실제 값은 여전히 같은 프로세스 메모리에 있고 마지막 어댑터 경계에서 드러납니다. 그리고 SecretRef로 설정하지 않은 평문 환경 변수는 이 장치 밖에 있습니다. 평문 그대로예요.
더 중요한 한계가 하나 더 있습니다. 문서가 "에이전트 접근 경계"라는 제목으로 따로 떼어 설명하는 내용입니다. SecretRef는 자격 증명이 설정 파일과 생성된 모델 파일에 남는 걸 막아 줍니다. 그러나 프로세스 격리 경계가 아닙니다. 에이전트가 읽을 수 있는 경로에 평문 자격 증명이 남아 있으면 파일 도구나 셸 도구로 그냥 읽힙니다. API 수준의 가리기를 통째로 건너뛰는 거죠.
그래서 문서는 마이그레이션이 끝났다고 말할 조건을 네 가지로 못 박아 놓았습니다. 지원되는 자격 증명이 평문 대신 SecretRef를 쓰고 있어야 합니다. openclaw.json, SQLite 인증 프로필 저장소, .env, 생성된 models.json에서 남은 평문이 걷혔어야 합니다. openclaw secrets audit --check가 깨끗해야 합니다. 남은 미지원·회전 자격 증명은 OS 격리나 컨테이너 격리 또는 외부 자격 증명 프록시로 따로 보호돼야 합니다.
백업, 복사해 둔 설정, 옛날 모델 카탈로그도 지우거나 에이전트 신뢰 경계 밖으로 옮기기 전까지는 그대로 운영 비밀이라고 문서가 경고합니다. 이 얘기는 OWASP LLM Top 10의 시스템 프롬프트 유출 항목과 결이 같습니다. 진짜 문제는 유출된 문서가 아니라 거기 자격 증명이 들어 있었다는 사실이죠. 그 정리는 OWASP LLM Top 10 글에 해 뒀습니다.
저장소 자체의 성질도 알아 두는 편이 낫습니다. 저장소 값은 저장 시점에 암호화되지 않습니다. 공유 상태 SQLite 데이터베이스 state/openclaw.sqlite에 평문으로 있고, 같은 데이터베이스의 다른 자격 증명과 똑같이 0600 파일·0700 디렉터리 권한으로만 보호됩니다. 저장 자체를 더 격리하고 싶으면 1Password 플러그인이나 Vault SecretRef 같은 외부 exec 제공자를 쓰라고 안내합니다.
exec은 스냅샷을 한 번만 찍습니다
이건 실제로 헷갈릴 만한 타이밍 문제입니다. 게이트웨이 호스트 exec은 한 실행 안에서 첫 실행 때 저장소 스냅샷을 한 번 찍습니다. 그 시점 전에 저장된 자격 증명은 들어갑니다.
그 뒤로는 추가, 교체, 삭제, 호스트 수정이 그 실행의 스냅샷을 새로 고치지 않습니다. 바뀐 걸 반영하려면 새 실행을 시작해야 합니다. 자격 증명 요청이 성공했다고 해서 이미 돌고 있는 exec 도구가 그 값을 쓸 수 있다는 뜻은 아니라고 문서가 직접 적어 놓았습니다. exec 쪽 승인과 허용 목록 구조는 exec 승인 글에 따로 다뤘어요.
보호된 시크릿은 프록시가 켜졌을 때만 게이트웨이 호스트 하위 프로세스 환경에 센티넬로 들어갑니다. 변수 이름은 저장한 항목 이름 그대로입니다. 그 변수는 물려받은 환경에서 읽어 쓰는 거고, 시크릿 템플릿을 따로 넣거나 변수를 덮어쓰거나 출력하면 안 됩니다. 프록시가 꺼져 있으면 보호된 항목은 아예 주입되지 않습니다. 그때는 설정 SecretRef를 쓰면 됩니다.
이 주입이 닿지 않는 경로도 명시돼 있습니다. 네이티브 하네스 셸, 샌드박스, 노드 실행은 보호된 값을 받지 않습니다. 그리고 문제 해결용 스위치 OPENCLAW_SECRET_SENTINELS=off는 보호 저장소 봉인을 끄지 못합니다. MCP 서버에 환경 변수로 자격 증명을 넘기는 경우도 이 규칙 안에서 봐야 합니다. MCP 연결 자체는 MCP 글에 정리해 뒀습니다.
실행이 닫히면 그 실행의 프록시 허가와 이미 열린 연결이 취소됩니다. 배경 하위 프로세스 터널도 같이요. 다만 이미 상위 전송에 넘어간 바이트는 되돌릴 수 없다고 적혀 있습니다. 취소는 앞으로를 막는 것이고 지난 것을 지우지는 않는다는 뜻이죠.
답이 안 오거나 요청이 취소될 때
승인 화면을 건너뛰거나 시간이 다 되면 에이전트는 no_answer를 받습니다. 그러면 막혔다고 말하거나 알아서 판단해서 계속 가야 합니다. 문서는 여기에 조건을 하나 더 붙여 놓았습니다. 그 상황에서도 자격 증명을 채팅에 붙여 달라고 요구하면 안 된다는 겁니다.
이건 지어낸 원칙이 아니라 이 도구가 존재하는 이유 그 자체입니다. 마스킹 입력이 있는데 채팅으로 우회하면 도구를 쓰는 의미가 없어지죠.
뭐부터 손댈까
아래는 제 판단이고 문서가 정한 순서는 아닙니다.
저라면 먼저 env로 넣어 둔 게 있는지 봅니다. 편해서 env에 넣은 API 키가 하나라도 있으면 그건 에이전트가 읽고 출력할 수 있는 값이니까요. 그다음에 openclaw secrets audit --check를 돌려서 평문 잔여물을 확인합니다. 이그레스 프록시는 그 두 개를 정리한 뒤에 켜도 늦지 않습니다. 기본값이 꺼짐인 데는 이유가 있고, 호스트를 정확히 묶는 작업이 따라오거든요.
그리고 자격 증명을 채팅으로 받는 습관이 이미 있으면 그게 제일 먼저 끊어야 할 것입니다. 저장소 구조를 아무리 잘 잡아도 사람이 채팅창에 붙여넣으면 그 대화 기록에 남습니다.
정리
secrets 도구는 "에이전트에게 키를 주지 않고 키를 쓰게 하는" 흐름입니다. 에이전트는 이름과 이유를 말하고, 사람이 마스킹된 칸에 넣고, 게이트웨이가 저장합니다. 에이전트 손에 남는 건 SecretRef와 메타데이터뿐이에요.
다만 이걸 격리라고 오해하면 안 됩니다. 센티넬도 프로세스 격리가 아니고 SecretRef도 파일 시스템 경계가 아닙니다. 에이전트가 읽을 수 있는 경로에 평문이 남아 있으면 그게 제일 약한 곳입니다. 공식 문서가 감사·설정·적용 흐름을 편의 도구가 아니라 보안 마이그레이션 관문이라고 부르는 이유도 거기 있습니다.
본문의 사실 관계는 모두 위에 적은 공식 문서 페이지에서 확인한 내용입니다. 한국 법령이나 인증 기준의 시행일·의무는 이 글에서 다루지 않았습니다. 그 부분은 원문 규정과 소관 기관 자료를 직접 확인하셔야 합니다.
'OpenClaw 자동화' 카테고리의 다른 글
| AI 마케팅 자동화, 글쓰기 전에 정해야 할 3가지 — 1인 개발자 운영 가이드 (0) | 2026.10.03 |
|---|---|
| API 키를 채팅창에 붙여넣지 않으려면|OpenClaw secrets 도구와 SecretRef 정리 (0) | 2026.10.01 |
| AI가 내 컴퓨터에서 명령을 돌리기 전|OpenClaw exec 승인·허용 목록·상시 허가 읽는 법 (0) | 2026.09.30 |
| 내가 고른 모델이 아닌 모델이 답했을 때|OpenClaw 모델 선택 순서와 폴백 규칙 (0) | 2026.09.29 |
| 답장 기다리는 중에 메시지를 또 보내면|OpenClaw 큐 모드 steer·followup·collect·interrupt (0) | 2026.09.28 |