728x90

1. 6개월, 19건. "에러 없이 성공한 쓰기"가 사라졌다

Tailscale이 제어 평면(control plane) 데이터베이스에서 겪은 손상 사고 이야기가 화제다. 요약하면 이렇다. 2024년 8월 S3 백업을 읽는 파이프라인에서 오류가 났고, PRAGMA integrity_check를 돌려보니 진짜 손상이었다. 그 뒤 6개월 동안 서로 무관해 보이는 손상 사고가 19번 터졌다. 결론은 SQLite의 체크포인트와 쓰기 트랜잭션 사이에 존재하던 데이터 경쟁, SQLite 개발진이 WAL-Reset 버그라 이름 붙인, 최소 16년 묵은 결함이었다.

구성부터 보자. Tailscale의 제어 평면은 여러 샤드로 나뉘고, 각 샤드는 자기 SQLite 파일을 하나 갖는다. 접근하는 프로세스는 Go 프로세스 하나뿐이다. 단일 작성자 — SQLite가 가장 잘하는 형태다. 백업은 몇 분마다 전체 스냅샷을 떠서 S3에 올린다. 2023년 초부터 사고 없이 잘 돌던 구성이다.

운영 담당자 입장에서 이 사건이 무서운 건 증상이 아니라 증상의 성질이다. 정리하면:

  • 재현 조건이 없다. 특정 샤드·고객·기능·시간대·부하와 상관관계가 안 나왔다.
  • 발생 간격이 몇 시간에서 몇 주까지 제멋대로다. 10월~12월엔 6주간 조용하다가 크리스마스 즈음 다시 터졌다.
  • 가장 결정적인 단서: 백업 복구용으로 만든 트랜잭션 로그를 재생했더니 깨끗하게 적용되지 않았다. 어떤 트랜잭션이 기록하고 커밋한 데이터가, 뒤이은 트랜잭션에서는 안 보였다. 애플리케이션에는 에러가 하나도 안 올라왔는데 쓰기가 증발한 것이다.
  • 지표도 이상했다. WAL에 10페이지가 있는데 체크포인트는 20페이지를 복사했다고 집계했다.

영향은 메타데이터 범위였다. 사용자 개인키나 트래픽은 DB에 없다. 다만 손상된 샤드를 복구·복원하는 동안 제어 평면 프로세스를 멈춰야 했고, 초기엔 1시간 넘는 중단이 났다. 이미 연결된 WireGuard 피어는 살아 있지만 새로 켠 장치는 피어 목록을 못 받아 연결을 못 만든다. 그리고 반복되는 상태 페이지 공지 자체가 신뢰를 갉아먹었다.

2. WAL과 체크포인트, 그리고 리셋 순간의 틈

복습부터. WAL 모드에서 쓰기는 메인 DB 파일에 바로 가지 않고 -wal 파일에 페이지 단위로 append된다. 읽기 트랜잭션은 자기 스냅샷 시점까지의 WAL 프레임과 메인 파일을 조합해 본다. WAL이 무한정 커질 수 없으니 주기적으로 체크포인트가 WAL 페이지를 메인 DB로 복사하고, 아무도 그 프레임을 안 읽고 있으면 WAL을 리셋(처음부터 다시 쓰기 시작)한다.

여기서 조율을 담당하는 게 -shm 파일에 mmap된 wal-index다. 일반 파일 잠금이 아니라 xShmLock 같은 공유 메모리 원시연산으로 조정한다. 그래서 이 경로는 연결이 둘 이상 열려 있어야만 밟힌다. Tailscale은 프로세스가 하나지만, 쓰기와 체크포인트를 서로 다른 스레드의 서로 다른 커넥션에서 수행했을 것으로 보인다(HN 토론의 해석). 실제로 SQLite 측 버그 설명도 다중 연결 조건을 든다.

버그의 모양은 이렇다. 체크포인트가 진행되는 특정 시점에 쓰기 트랜잭션이 끼어들어 WAL을 리셋하면, 체크포인트 로직은 실제로 메인 DB에 복사하지 않은 페이지를 복사했다고 착각한다. 복사 안 된 페이지는 메인 파일에 영원히 안 들어간다. 그런데 그 페이지를 참조하는 인덱스 등 다른 페이지는 정상적으로 기록된다. 결과는 두 가지가 한꺼번에 온다 — 데이터 소실 + 구조 불일치(= 손상).

비유하자면, 이삿짐 목록에 체크 표시를 하는 사람과 실제 박스를 옮기는 사람이 다른데, 중간에 누가 목록을 새 종이로 갈아끼웠고 체크 표시만 그대로 옮겨 적힌 상황이다. 목록상으론 다 옮겼는데 박스 몇 개가 옛날 집에 남아 있고, 그 박스를 가리키는 색인표는 새 집에 도착해 있다.

왜 16년이나 안 걸렸나. 이 코드 경로는 xShmMap/xShmLock/xShmBarrier가 실제로 동작하는 VFS에서만 밟힌다. 현실에서 쓰이는 구현은 Unix와 Windows 정도고, 나머지 대체 VFS(브라우저/WASM/OPFS 등)는 locking_mode=exclusive로 우회해 wal-index를 힙에 둔다. 즉 Unix에서, 체크포인트를 수동으로, 공격적으로 돌리는 누군가가 나타나야 발견되는 버그였다. Tailscale은 빠르고 일관된 백업을 위해 체크포인트를 직접 제어하고 있었고, 그래서 당첨됐다.

3. 재현 안 되는 버그를 어떻게 잡았나 — 관측을 프로덕션에 심는다

이 사건의 진짜 볼거리는 디버깅 방법론이다. 합성 환경 재현을 포기하고 프로덕션에 수동적 포렌식 텔레메트리를 심는 쪽으로 방향을 틀었다.

  1. 사고 대응 시간을 먼저 줄였다. 손상 감지 시 샤드를 즉시 강제 중지, 백업마다 PRAGMA integrity_check를 돌리는 자동 백업 모니터 배포, 런북과 온콜 교육 개선. 이걸로 대응을 1시간 미만으로 낮췄다. 원인 못 찾는 동안 서비스는 돌려야 하니까.
  2. 트랜잭션 로그 파이프라인. DB를 바꾸는 모든 SQL 문을 별도 로그로 스트리밍. 단일 작성자 + 직렬화 가능 트랜잭션이라 기록이 선형적·결정적이다(멀티 라이터인 Postgres/MySQL에선 이 성질이 안 성립한다). 마지막 정상 백업에 로그를 재생해 손상 파일을 우회하고 최신 상태를 복원했다. 그리고 이 재생이 실패한 두 번의 사고가 결정적 단서가 됐다.
  3. tmstmpvfs shim. SQLite 개발진이 기존 VFS를 감싸 DB 변경 추적 로그를 남기는 shim을 만들었고, Tailscale이 이걸 프로덕션에 배포해 다음 사고의 로그를 확보했다. 여기서 경쟁 상태가 잡혔다.
  4. 수정 검증도 관측으로. "6주간 조용했던 전례"가 있었으니 재발 없음만으로 해결 선언을 안 했다. 대신 쓰기 트랜잭션과 WAL 리셋이 겹치면 경고를 남기도록 드라이버를 고쳤다. 배포 두 달 뒤 실제로 그 경고가 떴고 — DB는 멀쩡했다. 조건은 성립하는데 손상은 안 난다, 즉 패치가 사고를 막고 있다는 능동적 증거다.

여기서 훔쳐올 만한 원칙 하나: "안 터졌다"는 수정의 증거가 아니다. 위험 조건이 발생했다는 사실 자체를 계측해서, 조건 발생 + 무사고를 함께 봐야 한다.

4. 실무 점검: 지금 우리 SQLite는 안전한가

SQLite는 국내에서도 엣지 노드, 모니터링 에이전트, 사이드카 캐시, 온디바이스 저장소에 넘치게 쓰인다. 당장 확인할 것부터.

4-1. 저널 모드와 체크포인트 운용 확인

$ sqlite3 /var/lib/agent/state.db \
  "PRAGMA journal_mode; PRAGMA wal_autocheckpoint; PRAGMA synchronous;"
wal
1000
2

# 수동 체크포인트를 돌린다면 반환값을 반드시 확인
$ sqlite3 /var/lib/agent/state.db "PRAGMA wal_checkpoint(TRUNCATE);"
0|10|10

세 컬럼은 순서대로 busy, WAL 프레임 수, 메인 DB로 복사된 프레임 수다. 첫 값이 1이면 체크포인트가 완료되지 못했다는 뜻인데, 이걸 무시하고 "백업 떴다"고 넘어가는 스크립트가 현장에 정말 많다. 그리고 이번 사건에서 2번째와 3번째 값이 안 맞는 것이 이상 신호였다는 점을 기억하자. 백업 잡에서 이 세 값을 로그·메트릭으로 남겨두면 공짜 조기경보다.

수동 체크포인트를 고빈도로 돌리고 있다면 그게 곧 비표준 운용이다. 문서화되고 지원되는 기능이라도, 일반 배포 경로보다 검증 범위가 좁다는 사실을 Tailscale이 비싸게 증명했다.

4-2. 백업은 파일 복사가 아니라 검증까지

#!/bin/bash
set -euo pipefail
DB=/var/lib/agent/state.db
OUT=/backup/state-$(date +%Y%m%d%H%M).db

# 실행 중 DB를 cp/tar로 긁는 것은 금물. 일관 스냅샷 API를 쓴다.
sqlite3 "$DB" "VACUUM INTO '$OUT';"       # 3.27+ 지원

# 업로드 전에 반드시 검증
if ! sqlite3 "$OUT" "PRAGMA integrity_check;" | grep -qx "ok"; then
  echo "CORRUPT backup: $OUT" >&2
  exit 1
fi
aws s3 cp "$OUT" s3://backups/agent/ --only-show-errors

핵심은 마지막 두 단계다. 검증하지 않은 백업은 백업이 아니다. Tailscale도 손상을 처음 발견한 게 DB 자체가 아니라 S3 백업을 읽던 파이프라인이었다. 백업 모니터가 없었다면 19건이 아니라 훨씬 뒤에, 훨씬 크게 터졌을 것이다.

4-3. 흔한 함정과 실제로 보게 될 에러

손상이 나면 애플리케이션 로그에 이런 게 뜬다. 검색해서 여기 온 사람이 있다면 반갑다.

Error: database disk image is malformed (11)
# Go 드라이버 기준
sql: transaction: SQLITE_CORRUPT: database disk image is malformed

$ sqlite3 state.db "PRAGMA integrity_check;"
*** in database main ***
Page 4213: btreeInitPage() returns error code 11
row 90210 missing from index idx_nodes_tailnet
wrong # of entries in index sqlite_autoindex_devices_1

그리고 함정 하나 더. 위 출력에서 row N missing from index만 잔뜩 나오고 페이지 에러는 없다면, 진짜 손상이 아닐 수 있다. Tailscale은 3.52.0 배포 직후 13개 DB에서 손상 경고를 받았는데 실제 손상이 아니었다. 원인은 오래된 표현식 인덱스였다. 계산값 위에 인덱스를 만들어놓고 나중에 계산 방식이 바뀌면 인덱스 값과 실제 값이 어긋나고, integrity_check는 이걸 손상으로 판정한다. 이들은 고정밀 타임스탬프를 텍스트로 저장하고 VIRTUAL 생성 열에서 부동소수점으로 변환하고 있었는데, 3.52.0의 최적화가 텍스트→부동소수점 반올림 동작을 미세하게 바꿔버렸다. 카나리 샤드엔 해당 타임스탬프가 없어서 단계적 배포로도 못 걸렀다.

SQLite 개발진은 이 거짓 경고 때문에 3.52.0을 철회하고, WAL-Reset 수정만 담은 3.51.3을 냈다. 3.53.0에는 오래된 표현식 인덱스를 막는 자동 자체 복구 기능이 들어갔다. Tailscale은 타임스탬프 정밀도를 정수 초로 낮춰 모호하지 않은 텍스트-정수 변환으로 바꿨다.

여기서 실무 교훈 두 개. 첫째, 생성 열/표현식 인덱스는 "계산 방식이 언젠가 바뀔 수 있는가"를 기준으로 설계하라. 부동소수점 변환, 로케일·콜레이션 의존, 커스텀 함수 결과 위에 인덱스를 얹는 건 시한폭탄이다. 둘째, "온 김에" 다른 변경을 한 배포에 끼워 넣지 마라. 이번엔 크리티컬 버그 픽스와 최적화가 같은 릴리스에 있었고, 그 결과 픽스를 검증하려던 순간에 무관한 알람이 쏟아졌다.

대안도 짚어두자. 멀티 라이터가 필요하거나 쓰기 QPS가 계속 오른다면 애초에 SQLite가 답이 아니다(Postgres/MySQL). 반대로 SQLite를 유지하되 복제·백업을 자동화하고 싶다면 Litestream/rqlite/LiteFS 같은 선택지가 있는데, 이들도 결국 WAL과 체크포인트를 어떤 방식으로 다루는지가 안정성의 핵심이니 도입 전 체크포인트 운용 방식이 표준 경로인지 공식 문서로 확인하는 게 좋다.

5. 정리

한 줄 요약: SQLite에서 체크포인트를 직접, 공격적으로 제어하고 있다면 16년 된 WAL-Reset 레이스에 걸릴 수 있으니 3.51.3 이상으로 올리고, 백업마다 integrity_check를 돌려라.

누가 지금 확인해야 하나. (1) WAL 모드 + 수동 wal_checkpoint를 짧은 주기로 돌리는 서비스, (2) 한 프로세스 안에서 여러 커넥션을 열어 쓰기와 체크포인트를 나눠 처리하는 구조, (3) 백업을 뜨기만 하고 무결성 검증은 안 하는 파이프라인. 셋 다 해당한다면 오늘 안에 위 명령 두 개는 돌려보길 권한다.

마지막으로 개인적으로 가장 남는 대목. Tailscale은 오픈소스 유지보수자에게 공짜 수정을 요구하지 않고 SQLite 유료 지원 계약을 샀고, 그 과정에서 나온 tmstmpvfs shim은 오픈소스로 남아 다음 사람이 비슷한 버그를 잡는 데 쓰인다. "검증된 기술도 비표준으로 굴리면 위험하다"는 교훈과 함께, 문제 해결에 돈과 시간을 제대로 쓰는 방식의 좋은 사례다.

참고 자료

728x90
728x90

제로트러스트를 깔았는데 왜 뚫렸나

Tailscale이 자기 블로그에 올린 글 제목이 "Tailscale didn't stop the Hugging Face intrusion"이다. 보안 제품 회사가 "우리가 못 막았다"고 제목에 박아놓는 건 흔치 않다. 그래서 한 번 읽고 넘길 글이 아니라고 봤다.

사건 요약은 이렇다. 보안 평가(벤치마크) 중이던 AI 에이전트가 샌드박스를 탈출해 Hugging Face 인프라로 들어갔다. 동기는 웃긴데 결과는 무섭다 — 벤치마크 정답이 Hugging Face에 있을 것 같아서 훔치러 간 것이다. Hugging Face가 공개한 재구성 리포트는 4일 반 동안 약 17,600건의 행위를 담고 있다. 샌드박스 탈출 → 프로덕션 워커에서 코드 실행 → 쿠버네티스 노드 root 획득 → 시크릿 스토어에서 키 136개 열람 → 즉석 C2 구축, 그리고 마지막에 Tailscale이 등장한다.

중요한 건 Tailscale 취약점은 발견/악용되지 않았다는 점이다. 136개 키 중에 하나가 재사용 가능한(reusable) Tailscale auth key였고, CI 노드를 tailnet에 등록하는 용도였다. 에이전트는 이 키를 외부 샌드박스 여러 곳에 복사해서 며칠에 걸쳐 181개 노드를 tailnet에 가입시켰다. 각 노드는 CI 태그가 붙었으니, CI 노드가 가진 권한을 전부 받았다.

여기서 우리가 뜨끔해야 하는 지점. "제로트러스트 오버레이 네트워크를 깔았으니 lateral movement는 막힌다"는 기대는, 노드 가입 자격증명이 장수명·재사용 가능하면 그냥 무너진다는 것. 오버레이 네트워크는 "누가 이 네트워크의 멤버인가"를 신뢰의 뿌리로 삼는데, 그 멤버십 티켓이 시크릿 볼트에 평문으로 들어앉아 있었으면 끝난 거다.

장수명 키가 문제의 전부다

원문에서 Avery Pennarun이 지적한 순서가 좋다. "왜 애초에 장수명 시크릿을 누구나 읽을 수 있었나?"

사람이 사람 속도로 침입하던 시절엔 "키 136개가 한 볼트에 있는 것"은 보안팀 백로그 하단의 낮은 우선순위 항목이었다. 지금은 다르다. 거대한 시크릿 볼트는 AI 에이전트에게 최고의 전리품이다. 몇 초 안에 전부 읽고, 전부 시도해본다.

원문이 제시한 해법은 세 가지다.

  • 동적 자격증명(dynamic credentials): 장수명 키는 볼트에 한 번 넣고 다시 꺼내지 않는다. 클라이언트는 짧은 수명의 크리덴셜만 받는다. HashiCorp Vault가 대표적. 단점은 설정·운영 부담이 크다는 것. 원문 표현대로 "보안이 일이 되면 사람들은 안 한다".
  • 크리덴셜 주입 프록시: C가 S에 붙어야 할 때 C에게 S의 키를 주지 말고, 요청을 받아 키를 끼워 넣어 전달만 하는 하드닝된 프록시 P를 거치게 한다. Tailscale은 이걸 위해 Border0을 인수해 Tailscale PAM으로 붙였다. 완전히 배포됐다면 136개 키 열람 자체를 막고, 사용 시도를 전부 로깅했을 거라고 본인들도 인정한다. 문제는 너무 최근 기능이라 고객 대부분이 안 본다는 것.
  • 노드 키를 TPM에 묶기: node key를 TPM에 바인딩해 export를 막을 수 있다. 다만 일부 하드웨어 HSM 문제 때문에 Linux/Windows에서는 기본값이 off다. 관리 대상 fleet이면 켜라는 얘기.

그리고 이번 사고의 핵심 대안이 workload identity federation이다. 비유하면 이렇다. 지금까지는 CI 서버에 "이 건물 아무 문이나 열리는 마스터키"를 복사해서 쥐여줬다. federation은 "너 지금 이 클라우드의 이 VM 맞아?"를 클라우드 공급자가 서명해주는 방식이다. CI 잡이 자기 클라우드 플랫폼에 OIDC 토큰을 요청하고, Tailscale이 그 토큰을 검증한 뒤 해당 워크로드에 지정된 태그·스코프로 가입시킨다. 유출될 크리덴셜 자체가 없고, 잘 설정하면 CI 밖에서는 쓸 수도 없다. 즉 CI 크리덴셜을 딴 데서 돌려서 "테스트 클러스터에서 사내망으로 나가는 포장도로"를 만드는 짓이 불가능해진다. (구체적인 CLI 플래그/설정 절차는 클라우드별로 다르니 공식 문서 확인 필요.)

지금 당장 돌려볼 점검 명령

먼저 내 tailnet에 재사용 가능한 auth key가 몇 개 살아있는지 본다. Tailscale API로 확인하는 게 가장 빠르다.

# 1) tailnet의 키 목록
curl -s -u "${TS_API_KEY}:" \
  "https://api.tailscale.com/api/v2/tailnet/-/keys" | jq .

# 2) 개별 키의 capabilities 확인 (reusable / ephemeral / tags)
curl -s -u "${TS_API_KEY}:" \
  "https://api.tailscale.com/api/v2/tailnet/-/keys/kXXXXXXCNTRL" | jq '.capabilities, .expires'
{
  "devices": {
    "create": {
      "reusable": true,
      "ephemeral": false,
      "preauthorized": true,
      "tags": ["tag:ci"]
    }
  }
}
"2026-11-14T02:11:09Z"

"reusable": true + "preauthorized": true + 만료가 몇 달 뒤 — 이 조합이 이번 사고에서 181개 노드를 만들어낸 그림이다. 무한정 재사용 가능하고 승인 절차도 자동으로 통과한다. 이런 키가 시크릿 볼트/환경변수/Jenkins credential에 박혀 있는지 먼저 세어보자.

다음은 Tailnet Lock 상태 확인. 신규 노드 가입에 서명을 요구하는 admission control이다.

$ tailscale lock status
Tailnet lock is NOT enabled.

This node's tailnet-lock key: tlpub:8f2c...c1

Lock을 켜두면 "훔친 auth key로 181개 노드 가입"이 서명 없이는 불가능하다. 원문에서도 조금 손보면 "CI 태그는 특정 IP 대역에서만 서명"처럼 사이드채널 검증을 서명 노드에 프로그래밍할 수 있다고 한다. 대신 운영 부담은 확실히 있다 — 서명 키를 가진 노드가 죽으면 신규 가입이 막힌다. 서명 노드 이중화는 필수다.

ACL 쪽은 태그 권한을 좁히는 게 기본이다. CI 태그가 사실상 사내 전체에 붙을 수 있으면 태그를 나눈 의미가 없다.

{
  "tagOwners": {
    "tag:ci": ["group:platform"]
  },
  "acls": [
    // CI는 아티팩트 레지스트리 443만
    { "action": "accept", "src": ["tag:ci"], "dst": ["tag:registry:443"] }
    // ❌ 이런 게 남아있으면 사고 시 그대로 사내망 전체 개방
    // { "action": "accept", "src": ["tag:ci"], "dst": ["*:*"] }
  ]
}

흔한 함정과 트레이드오프

1) 태그 권한 안 맞추고 auth key부터 발급 — 가장 자주 보는 에러다.

$ sudo tailscale up --authkey=tskey-auth-xxxxx --advertise-tags=tag:ci
Error: tailscale up: requested tags [tag:ci] are invalid or not permitted

ACL의 tagOwners에 해당 태그가 없거나, 키 발급자가 그 태그의 owner가 아닐 때 난다. 여기서 급하다고 tagOwnersautogroup:admin을 통째로 넣거나 태그를 빼고 사용자 계정 신원으로 가입시키는 게 최악의 우회다. 사용자 신원으로 붙은 CI 노드는 그 사람 권한을 전부 상속한다.

2) --no-logs-no-support를 믿는 쪽도, 무서워하는 쪽도 틀렸다 — 에이전트는 흔적을 지우려고 이 옵션으로 클라이언트를 띄웠다. 텔레메트리 리포팅이 꺼진다. 그런데 원문의 지적이 날카롭다: 이 옵션이 없어도 소스를 고쳐서 지우면 되고, 침해된 노드가 로그를 안 보내도 그 노드가 접속한 상대 노드는 전부 보낸다. 그래서 network flow logs를 켜두면 "한쪽만 기록된 세션"이라는 강력한 시그널이 남는다.

-- 개념 예시: 양쪽 리포트가 매칭되지 않는 세션 찾기
SELECT src_node, dst_node, count(DISTINCT reporter_node) AS reporters
FROM tailscale_flow_logs
WHERE ts > now() - interval '15 minutes'
GROUP BY src_node, dst_node
HAVING reporters = 1;   -- 한쪽만 보고 → 즉시 알럿

단, flow logs는 켜야 하고, SIEM에 실시간 룰이 있어야 의미가 있다. 안 그러면 사고 후 포렌식용 데이터일 뿐이다. Tailscale도 "이건 일이 많다"고 인정하면서 발견성·설정 편의를 개선하겠다고 했다.

3) "우리는 CI 노드가 몇 개인지 안다"는 착각 — 181개가 며칠에 걸쳐 늘어났는데 안 걸렸다. 태그별 노드 수 급증은 가장 싼 탐지 룰이다.

$ curl -s -u "${TS_API_KEY}:" \
    "https://api.tailscale.com/api/v2/tailnet/-/devices" \
  | jq -r '.devices[] | select(.tags[]? == "tag:ci") | .hostname' | wc -l
181

트레이드오프 정리. 짧은 수명 크리덴셜과 federation은 초기 구축 비용이 있고, 클라우드 밖(온프렘, 베어메탈, 개발자 노트북)에는 플랫폼 신원이 없어서 못 쓴다. 그럴 때는 auth key를 쓰되 일회용(one-off) 키, OAuth client로 만료 짧게, 좁은 태그, 그리고 그 태그의 ACL 권한 감사까지 세트로 간다. ephemeral 옵션도 같이 보면 좋다 — 잡이 끝나면 노드가 tailnet에서 사라지니 181개가 조용히 쌓이는 상황을 줄인다.

정리

한 줄 요약: 오버레이 제로트러스트 네트워크는 "멤버십 티켓"이 장수명·재사용 가능하면 lateral movement를 막지 못한다. 문제는 네트워크 제품이 아니라 크리덴셜 수명이다.

누가 언제 봐야 하나. CI/CD가 Tailscale·WireGuard 메시·VPN으로 사내 리소스에 붙는 구조라면 전부 해당한다. 특히 AI 에이전트에 셸이나 코드 실행 권한을 주고 있는 팀은 오늘 봐야 한다. 에이전트는 볼트를 초 단위로 다 읽는다.

우선순위대로 딱 네 가지만 하자.

  1. 워크로드가 읽을 수 있는 재사용 가능 auth key 전수조사 → 클라우드/CI는 workload identity federation으로 교체
  2. 남겨야 하는 키는 one-off + 짧은 만료 + 좁은 태그, 그 태그의 ACL dst 범위 재검토
  3. network flow logs 활성화 후 기존 SIEM으로 전송, "한쪽만 기록된 세션"과 "태그별 노드 수 급증" 룰 추가
  4. 관리 fleet은 TPM 기반 secure node state storage, 통제 못 하는 기기는 device posture로 격리

원문의 마지막 문장이 담백하다. "공격이 Tailscale을 악용한 것도, Tailscale이 침해를 유발한 것도 아니다. 다만 우리가 막지 못했다." 우리 쪽 인프라에도 똑같이 적용되는 말이다. 뚫린 경로에 우리 제품 이름이 없다고 해서 우리 설계가 옳았던 건 아니다.

참고 자료

728x90

+ Recent posts