요약. 지난주에 AI 에이전트가 9초 만에 PocketOS의 프로덕션 데이터베이스를 삭제했습니다. 저는 제 인프라를 동일한 방식으로 실패하게 만들어 보려고 했습니다. 여섯 가지 안전장치가 유지되었고, 하나의 솔직한 허점이 남아 있습니다.
- 128 GB 프로덕션 포크, 처음부터 끝까지: 7.2초. CoW reflink 자체: 2.3초.
- 에이전트는 grand(프로덕션) 저장소에서 차단되었고, 자체 재정의 설정도 차단되었으며, 접근이 허가되었을 때는 커널 샌드박스(비권한 사용자, 별도의 마운트 네임스페이스, 스코프된 Docker 소켓)에 격리되었습니다.
- Rediacc가 격리하지 않는 것: 저장소 데이터 내의 외부 SaaS 자격 증명. 포크가 이를 상속합니다. (업데이트. 2026년 5월:
rdc repo secret으로 이제 자격 증명을 저장소 이미지 외부에 완전히 보관할 수 있습니다. 새 마지막 섹션 “업데이트: 허점을 이제 해결할 수 있습니다”에서 전체 내용을 확인하세요.) 그 부분은 Rediaccfile 수명 주기 훅을 통해 개발자가 처리할 책임입니다.
업데이트. 2026년 5월. 이 게시물이 발행된 이후, Rediacc는 저장소별 시크릿(
rdc repo secret)이라는 일급 기능을 출시했습니다. 아래에서 설명한 “솔직한 허점”, 즉 저장소에 베이크인된 외부 SaaS 자격 증명이 이제 기본적으로 LUKS 이미지 외부에 시크릿을 보관하는 내장된 답을 가지게 되었으며, 포크는 기본적으로 어느 것도 상속하지 않습니다. 무엇이 출시되었는지, 무엇이 변경되었는지, 그리고 남아 있는 잔여 위험이 무엇인지 확인하려면 새 마지막 섹션 업데이트: 허점을 이제 해결할 수 있습니다로 이동하세요.
지난 주말, Jer Crane이 30시간의 포스트모템을 발행했습니다. Anthropic의 Claude Opus 4.6을 실행하는 Cursor 에이전트가 Railway의 프로덕션 데이터베이스를 삭제했습니다. 삭제는 단일 GraphQL 호출이었습니다. 9초가 걸렸습니다. Railway가 동일한 볼륨 내에 백업을 저장하기 때문에 Railway의 볼륨 백업도 함께 사라졌습니다.
그의 회사 PocketOS는 렌터카 업체가 일상 업무를 운영하는 데 사용하는 소프트웨어를 만듭니다. 그 업체들 중 일부는 5년 동안 PocketOS를 사용해 왔습니다. 토요일 아침, 고객들이 차를 가지러 왔을 때 렌터카 업체들은 그들이 누구인지 기록이 없었습니다. 3개월치 예약이 사라졌습니다. Jer는 Stripe 결제 내역과 이메일 확인서에서 복구할 수 있는 것을 하루 동안 재구축했습니다.
저는 그의 게시물을 두 번 읽었습니다. The Register, Tom’s Hardware, Business Standard가 모두 다루었습니다. Hacker News 스레드는 874개의 댓글을 받았습니다.
저는 다른 종류의 인프라 플랫폼을 만듭니다. 우리는 그것을 Rediacc라고 부릅니다. 구축된 방식의 핵심은 바로 이 시나리오를 더 어렵게 만드는 것입니다. 그래서 앉아서 테스트를 실행했습니다.
이 게시물은 제가 발견한 내용입니다. 숫자는 실제입니다. 오류 메시지는 CLI에서 인용했습니다. 그리고 Rediacc가 전혀 보호하지 않는 한 곳도 여기 있습니다. 그렇지 않은 척하는 것이 사람들을 곤경에 빠뜨립니다.
실제로 무엇이 빠져 있었나
Jer의 타임라인을 주의 깊게 읽으면 네 가지 실패가 겹쳐 있습니다.
- Cursor가 사용한 Railway API 토큰은 커스텀 도메인 관리를 위해 만들어졌습니다. 그런데
volumeDelete권한도 있었습니다. Railway의 CLI 토큰에는 작업별 스코핑이 없습니다. - Railway의 GraphQL API는
volumeDelete를 단일 POST로 받아들입니다. 확인 단계가 없습니다. - Railway의 “볼륨 백업”은 동일한 볼륨 내에 있습니다. 볼륨이 사라지면 백업도 사라집니다.
- Cursor 에이전트는 스스로 스테이징의 자격 증명 불일치를 수정하는 올바른 방법이 볼륨을 삭제하는 것이라고 결정했습니다.
잠깐 실패 4를 꺼내 보겠습니다. Cursor의 시스템 규칙은 에이전트에게 명시적인 사용자 요청 없이 파괴적인 git 명령을 절대 실행하지 말라고 했습니다. 삭제 후 설명을 요청받자 에이전트는 서면 자백을 했습니다. 데이터베이스 볼륨을 삭제하는 것이 “가장 파괴적이고 되돌릴 수 없는 행동: 강제 푸시보다 훨씬 나쁜”임을 인정하고 자신이 어긴 모든 안전 규칙을 나열했습니다.
프롬프트의 행동 규칙은 조언입니다. 강제가 아닙니다. 실패 1, 2, 3은 인프라 설계 선택입니다. 이것들이 실패 4를 실수에서 회사 손실로 바꾸는 것입니다.
테스트 설정
Rediacc에는 제가 운영하는 실제 프로덕션 머신 hostinger가 있습니다. 메일 서버, 자체 호스팅 GitLab, 관찰 스택, 벤치마크에 사용하는 128 GB StackOverflow 데모 등 13개의 저장소가 있습니다. 디스크는 87% 가득 차 있습니다. 여유 공간이 거의 없습니다. 실수가 아프게 다가오는 종류의 머신입니다.
저는 의도적으로 StackOverflow 데모를 선택했습니다. 박스에서 가장 큰 저장소입니다. 컨테이너와 영구 데이터가 있는 실제 애플리케이션처럼 설정되어 있습니다. 포크가 빠르고 격리된다면, 더 작은 모든 것에 대해서도 빠르고 격리됩니다.
테스트를 위한 에이전트는 Claude Opus를 실행하는 Claude Code였습니다. Cursor와 동일한 모델 계열입니다. Jer의 에이전트가 가진 것과 동일한 종류의 접근 패턴. 제가 구동한 CLI는 저희 자체 rdc입니다.
시도 1: 프로덕션 저장소에 직접 SSH 접속
에이전트(이 경우 저)가 처음 시도한 것은 가장 자연스러운 것이었습니다. 프로덕션 저장소에 셸을 열고 둘러보는 것입니다.
$ rdc term connect -m hostinger -r demo-stackoverflow -c "ls -la"
CLI가 거부했습니다. 그대로:
“demo-stackoverflow”는 grand(프로덕션) 저장소입니다. 에이전트는 grand 저장소를 직접 수정할 수 없습니다.
Grand 저장소는 프로덕션 데이터를 포함합니다. 대신 포크를 사용하세요. 포크는 안전하고 격리된 샌드박스 복사본입니다.
이것은 시스템 프롬프트가 아닙니다. 노트북을 떠나기도 전에 호출을 거부한 CLI 자체입니다. CLI는 제가 에이전트임을 보았습니다. Claude Code는 CLAUDECODE 환경 변수를 설정합니다. CLI는 또한 그 변수를 숨기려는 에이전트를 잡기 위해 /proc를 통해 프로세스 트리를 탐색합니다. 그런 다음 정책 테이블에 대한 작업을 매칭했습니다. 그런 다음 거부했습니다.
그래서 에이전트는 에이전트가 할 법한 일을 합니다. 스스로 재정의를 설정하려고 합니다.
$ REDIACC_ALLOW_GRAND_REPO=demo-stackoverflow rdc term connect ...
여전히 거부됩니다:
“demo-stackoverflow”는 grand(프로덕션) 저장소입니다. 에이전트가 시작한 재정의는 허용되지 않습니다.
REDIACC_ALLOW_GRAND_REPO를 설정하려고 시도하지 마세요. 에이전트가 시작되기 전에 사용자만 이것을 허가할 수 있습니다.
동일한 /proc 탐색이 두 가지 역할을 합니다. 먼저 에이전트를 발견합니다. 그런 다음 재정의가 에이전트 내부에서 설정되었는지 그 위에서 설정되었는지 확인합니다. 경계 아래: 거부됨. 위: 허용됨.
저는 이것을 테스트했습니다. 에이전트를 종료했습니다. 제 셸에서 export REDIACC_ALLOW_GRAND_REPO=demo-stackoverflow를 실행했습니다. Claude Code를 다시 시작했습니다. 그러자 연결이 작동했습니다. 비권한 rediacc 시스템 사용자(UID 7111)로 저장소에 들어갔습니다. DOCKER_HOST는 부모 저장소의 스코프된 Docker 데몬 소켓을 가리켰습니다.
demo-stackoverflow 재정의가 활성화된 상태에서 다른 프로덕션 저장소 nextcloud에 연결하려고 했습니다. 거부됨. 재정의는 저장소별이지, 마스터 스위치가 아닙니다.
시도 2: 저장소를 포크하고 포크에서 작업
이것이 Rediacc가 실제로 원하는 워크플로입니다.
$ time rdc repo fork --parent demo-stackoverflow -m hostinger --tag agent-test
터미널에서 복사한 출력:
Config loaded (9ms)
Connected (1.1s)
Renet provisioned (1.2s)
Machine verified (464ms)
License activated (2.1s)
✔ CoW clone complete (2.3s)
Total: 7.2s
2.3초만에 128 GB 포크가 완성되었습니다. 이유는 BTRFS reflink입니다. 포크는 한쪽이 쓰기 전까지 부모의 블록을 가리키는 메타데이터입니다. 데이터가 복사되지 않습니다.
스케일링을 확인하기 위해 2 GB 저장소에서 동일한 포크를 실행했습니다:
✔ CoW clone complete (573ms)
Total: 6.3s
2 GB 포크의 복제 단계는 573밀리초가 걸렸습니다. 128 GB 포크의 복제 단계는 2.3초가 걸렸습니다. 따라서 데이터 복제는 선형이 아니라 서브선형입니다. 엄밀히 말해 상수가 아닙니다. 하지만 총 포크 시간은 거의 동일합니다(6.3초 대 7.2초). 대기 시간의 대부분이 SSH와 라이센스 과정이지, 데이터가 아니기 때문입니다. 사용자 입장에서 포크는 저장소 크기에 관계없이 6~7초의 일시 중지입니다.
이제 포크에서 작업 중입니다. 자체 GUID(22c57178-...), 자체 networkId(21760), 자체 마운트 경로, 자체 Docker 데몬 소켓(/var/run/rediacc/docker-21760.sock)이 있습니다. 부모의 데이터는 reflink를 통해 공유됩니다. 부모 자체는 내내 계속 실행됩니다.
포크에 term connect할 때 이것이 보입니다:
$ rdc term connect -m hostinger -r demo-stackoverflow:agent-test -c "id"
uid=7111(rediacc) gid=7111(rediacc) groups=7111(rediacc),988(docker)
제 SSH 사용자가 아닙니다. 자체 셸이 없는 전용 rediacc 시스템 사용자, UID 7111입니다. 세션은 자체 마운트 네임스페이스에서 실행됩니다. /proc/self/ns/mnt를 확인했습니다. 포크 세션 내에서 부모의 마운트 경로를 나열하려 했을 때 ls는 Permission denied를 반환했습니다. 에이전트는 경로를 추측해서라도 프로덕션 데이터에 문자 그대로 접근할 수 없습니다.
이것이 설계에서 가장 중요한 부분입니다. 에이전트는 커널 수준 샌드박스를 받습니다. “제발 하지 마세요” 지시가 아닙니다. 에이전트가 포크 내에서 rm -rf /를 실행하기로 결정한다면, 최악의 경우는 부모에서의 2.3초 재포크입니다.
플랫폼 지원에 관한 참고 사항
재정의는 Linux에서만 작동합니다. 적법성 검사는 /proc/<pid>/environ이 필요합니다. 그 파일은 각 프로세스가 어떻게 시작되었는지에 대한 커널의 기록입니다. macOS와 Windows에는 동등한 것이 없습니다. 재정의가 에이전트가 아닌 본인에 의해 설정되었는지 확인할 방법이 없으면 CLI는 닫힘 방향으로 실패합니다. 올바르게 설정된 재정의도 해당 플랫폼에서는 거부됩니다.
오류 메시지가 무엇을 해야 하는지 알려줍니다:
REDIACC_ALLOW_GRAND_REPO 재정의는 darwin에서 지원되지 않습니다. … 재정의를 사용하려면 Linux에서 에이전트를 실행하세요(직접, WSL, Docker 또는 VM).
실제로 macOS나 Windows의 에이전트는 포크 우선 워크플로에서 벗어날 수 있는 탈출구가 없습니다. 이것은 의도적입니다.
이 테스트에서 유지된 안전장치
한두 가지 안전 속성을 검증하러 들어갔습니다. 여섯 가지를 가지고 나왔습니다. 각각 제가 가리킬 수 있는 코드와 인용할 수 있는 오류 메시지가 있습니다.
- Grand 저장소 차단. 에이전트는 grand(프로덕션) 저장소에서 직접 작업할 수 없습니다. 포크해야 합니다.
- 에이전트 설정 재정의 거부. 사용자가 설정할 수 있는 재정의 환경 변수는 에이전트 자체 환경에 나타나면 거부됩니다.
- 저장소별 재정의 스코핑.
demo-stackoverflow에 대한 허가는nextcloud에 아무 효과가 없습니다. 스코프는 목록이지, 플래그가 아닙니다. - 커널 샌드박스. 유효한 재정의가 있어도 세션은
rediaccUID로 실행되며, 자체 마운트 네임스페이스에서, 하나의 저장소 데몬으로 스코프된DOCKER_HOST로 실행됩니다. 다른 저장소를 볼 방법이 없습니다. - 온라인 포킹. 부모는 포크 내내 계속 실행되었습니다. 다운타임 없음, 절체 없음.
- 서브선형 포크 타이밍. 128 GB에 2.3초. 2 GB에 573ms. 대기의 대부분은 SSH 과정이지 데이터가 아닙니다.
Rediacc가 격리하지 않는 한 가지
이제 게시물의 더 어려운 부분입니다.
Rediacc는 인프라를 격리합니다: 디스크의 파일, Docker 데몬, 마운트 네임스페이스, 네트워크. 저장소가 자격 증명을 보유하는 외부 SaaS API는 격리하지 않습니다.
포크는 부모의 바이트 단위 BTRFS reflink입니다. 부모의 data/, .env, 또는 secrets/에 있는 모든 것이 포크에도 있습니다. 저장소에 STRIPE_LIVE_KEY, AWS_ACCESS_KEY_ID, 또는 Railway API 토큰이 포함되어 있다면 포크의 에이전트가 그것들을 읽을 수 있습니다. 해당 토큰으로 api.stripe.com 또는 s3.amazonaws.com 또는 backboard.railway.app을 호출할 수 있습니다. 외부에서 보면 그 호출은 프로덕션에서 온 것처럼 보입니다. Stripe나 AWS는 포크를 구분할 수 없습니다.
이것이 공동 책임의 경계입니다. Rediacc는 인프라 절반을 처리합니다. 외부 서비스 절반은 애플리케이션 코드에 있습니다.
세 가지 패턴이 개발자 측의 허점을 해결합니다:
- 저장소에 프로덕션 외부 자격 증명을 전혀 저장하지 않습니다. 컨테이너 시작 시 시크릿 관리자에서 가져옵니다. 포크의 컨테이너는 설계상 샌드박스 스코프 자격 증명을 가져옵니다.
- Rediaccfile
up()훅을 통해 포크 시점에 자격 증명을 제거하거나 교체합니다. 포크의up()은 부모와 다른 저장소 GUID에 대해 실행됩니다. 이를 감지합니다. 그런 다음.env를 샌드박스 값으로 다시 씁니다. - 포크별 외부 리소스를 프로비저닝합니다: 포크별 Stripe 샌드박스 계정, 포크별 테스트 데이터베이스, 포크별 S3 버킷.
PocketOS가 Rediacc에 있었다면 Railway API 토큰은 적절한 비교가 아니었을 것입니다. 그들의 인프라는 Rediacc 포크 자체였을 것입니다. Rediacc는 인증된 에이전트에게 volumeDelete에 해당하는 것을 노출하지 않기 때문에 찾을 Railway 토큰이 없었을 것입니다. 에이전트는 부모를 삭제할 경로가 없는 포크별 Docker 소켓 내에 있었을 것입니다.
하지만 에이전트가 자격 증명 파일에서 Stripe 프로덕션 키를 찾았다면 Rediacc는 에이전트가 실제 고객 카드에 환불을 발행하는 것을 막지 못했을 것입니다. 그것은 실제 손실입니다. 두 가지 모두 사실입니다.
업데이트: 허점을 이제 해결할 수 있습니다
위 섹션은 원래 작성한 대로 남겨두었습니다. 당시에 솔직함이 의미가 있었습니다. 발행 이후, 우리는 빠진 조각을 출시했고 허점은 이제 해결 가능합니다. 포크가 동작하는 방식을 변경하는 것이 아니라, 암호화된 저장소 이미지 외부에 있는 자격 증명을 넣을 곳을 추가함으로써입니다.
메커니즘은 rdc repo secret입니다. GitHub Actions 시크릿을 모델로 합니다. 구축된 방식에 대해 두 가지가 중요합니다.
시크릿은 LUKS 이미지 내부에 없습니다. 디스크의 별도 평면에 있습니다: env 모드 시크릿은 compose 파일의 ${REDIACC_SECRET_<KEY>} 보간으로 컨테이너에 전달되고, file 모드 시크릿은 Docker compose의 secrets: 블록을 통해 /run/secrets/<key>의 tmpfs 파일로 컨테이너에 전달됩니다. 어떤 방식이든 값은 저장소의 암호화된 이미지에 기록되지 않습니다. 포크의 BTRFS reflink는 이미지를 복사합니다. 이미지에 없었던 것은 복사하지 않습니다. 새로운 포크의 시크릿 목록은 비어 있습니다. 컨테이너는 프로덕션 자격 증명 없이 부팅되므로 프로덕션 호출 자체가 가능하지 않습니다.
모델은 쓰기 전용입니다. rdc repo secret get은 값이 아닌 SHA-256 다이제스트를 반환합니다. CLI를 통해 시크릿을 다시 읽을 방법이 없으며, 이것은 의도적입니다. 이는 터미널 녹화, 셸 기록, 실수로 인한 리디렉션의 누출 표면을 닫습니다. 교체는 이전 값을 확인하기 위한 --current <이전값>, 또는 이전 값이 없을 때 전제 조건을 건너뛰는 --rotate-secret(교체로 감사됨)입니다. 인간과 에이전트 모두 대칭입니다. 두 게이트 모두 동등하게 적용됩니다.
PocketOS 시나리오에 대한 의미: 시크릿을 적용한 Rediacc에 있었다면 포크 내의 에이전트는 자격 증명 파일에서 STRIPE_LIVE_KEY를 찾지 못했을 것입니다. 포크의 시크릿 맵은 비어 있었을 것입니다. 샌드박스 내에서 Stripe를 호출할 것이 없었을 것입니다. 위 원래 섹션의 “실제 손실” 한정어는 “이미지에 자격 증명을 베이크인한 저장소에 대한 실제 손실”로 줄어듭니다. 그리고 해당 저장소에 대한 수정은 새 메커니즘으로의 일회성 마이그레이션입니다.
솔직한 잔여 허점. rdc repo secret을 도입하기 전에 저장소 이미지에 작성한 시크릿. 볼륨에 커밋된 .env, 데이터베이스에 지속된 자격 증명, 토큰이 있는 YAML 파일. 이것들은 여전히 이미지 데이터의 일부입니다. CoW reflink는 이전처럼 이것들을 전파합니다. 메커니즘은 그것들을 안전하게 넣을 곳을 제공합니다. 암호화된 볼륨 내부 디스크에 이미 있는 것을 소급하여 제거하지는 않습니다. 오늘 수정은 수동 이전입니다(값을 읽고, rdc repo secret을 통해 설정하고, 이미지 소스에서 지웁니다). 일급 rdc repo secret lift --from .env는 로드맵에 있습니다.
에이전트 위협 모델에 대해 구체적으로, 이 설계의 두 가지 부작용이 주목할 만합니다. 첫째, repo secret list와 repo secret get은 MCP 도구로 노출됩니다(읽기 안전. 이름과 다이제스트, 절대 값 아님). 따라서 앱 설정을 도와주는 에이전트가 무엇이 구성되어 있는지 그것이 무엇인지 보지 않고도 발견할 수 있습니다. 둘째, 쓰기가 전제 조건에 실패하면 JSON 오류 봉투에 에이전트가 인간에게 그대로 전달해야 할 구체적인 명령이 있는 구조화된 next.options[].run 필드가 포함됩니다. “이전 값을 잊어버렸습니다”에 대한 교체 탈출구도 포함하여. 전체 모델은 AI 에이전트 안전을 참조하세요.
전체 방법은 저장소 § 시크릿에 있습니다. 레시피는 짧습니다: 시드하려면 rdc repo secret set --name <repo> --key <KEY> --value <val> --current "", compose에서 사용하려면 ${REDIACC_SECRET_<KEY>}, rdc repo fork로 포크의 시크릿 목록이 비어 있는지 확인합니다.
이런 작업을 하는 사람에게 무엇이 달라지나
프로덕션 환경에 그것을 삭제할 수 있는 자격 증명으로 AI 에이전트 셸 접근을 제공한다면, 문제는 그것이 결국 파괴적인 일을 할지 여부가 아닙니다. 언제, 그리고 얼마나 복구 가능한지입니다.
Rediacc에서 달라지는 것: 파괴적인 피해 반경이 포크로 제한됩니다. “잘못된 것을 삭제”하는 실수의 비용은 2.3초 재포크입니다. 에이전트가 “수정”하기로 결정한 자격 증명 불일치의 비용도 같은 2.3초 재포크입니다. 커널 샌드박스는 대부분의 실수가 프로덕션 데이터에 도달하지 못하게 합니다.
달라지지 않는 것: 저장소에 실제 외부 자격 증명이 있으면 에이전트가 그것을 사용할 수 있습니다. 그것은 인프라 레이어가 아닌 애플리케이션 레이어에서 수정해야 할 여러분의 몫입니다.
Rediacc가 PocketOS 사건의 모든 부분을 막았을 것이라고 가장하지 않겠습니다. PocketOS 이야기의 최악의 부분은 실제 백업이 없는 Railway 데이터 삭제였습니다. Rediacc에서는 그런 일이 일어나지 않았을 것입니다. 우리는 어떤 에이전트에게도 접근할 volumeDelete API를 제공하지 않기 때문입니다. 남아 있는 위험 표면, 에이전트가 코드베이스의 자격 증명으로 호출할 수 있는 SaaS API, 그것이 up() 훅에 있는 안전 이야기의 일부입니다. 저희 격리 모델에 있는 것이 아닙니다.
전체 숫자, 그대로의 오류 메시지, 제가 확인한 코드 경로는 AI 에이전트 안전 및 안전장치에 문서화되어 있습니다. 자신의 인프라에서 유사한 테스트를 실행하고 싶다면 포크 워크플로는 저장소에 있습니다. 약 7초가 걸립니다.