13 단계 플랫폼 지키기 — 한도 · 규칙 · 보안
이 페이지 목차
목적만든 순서1 · 하루 5만 읽기 — 태운 건 직원이 아니라 만드는 쪽이었다 (9/19~9/28)2 · 무거운 것을 DB 밖으로 — 첨부 이관과 옛 기록 떼기 (9/19~9/25)3 · 사본 셋과 열쇠 둘 — 백업은 영영 읽기 전용 (9/19~9/25)4 · 강제 장치 셋 (9/20)5 · 규칙 시험대와 올리는 순서 (9/20~9/28)6 · 고친 게 안 닿던 이유 — 서비스워커와 판 번호 (9/21~9/26)7 · 충돌 해소와 push 를 잇지 마라 (9/26)8 · 일반 직원 눈으로 — 보안 전수 대조 (9/26~9/27)9 · 저장소 비공개와 사이트 이사 (9/27)10 · 결재 서명 위조 막기 · NCR·CAR "즉시 반영 안 됨" (9/28)💡 설명함정과 배운 것출처뒷이야기 · 13 / 17단계 · 2026-09-19 ~ 09-28 · 갈래 SJ FLOW
이전 단계: 12 SJ 메신저와 AI 비서 — 직원 모두의 창구 · 다음 단계: 14 사무직 AI 작업실 — 직원마다 개인 비서
핵심 요약
| 항목 | 내용 |
|---|---|
| 이 단계에서 한 일 | 무료 요금제의 하루 읽기 한도(5만 건) 안에서 회사 플랫폼이 멈추지 않게 하고, 일반 직원의 눈으로 구멍을 찾아 막았다. 한도는 세고(장부·계량기), 무거운 것은 DB 밖으로 빼고(첨부·옛 기록), 규칙은 시험대에서 먼저 돌리고, 구멍은 일반 직원 계정으로 훑었다. |
| 기간 | 2026-09-19 ~ 09-28 (10일). 12단계와 날짜가 겹친다 — 두 단계의 커밋 수를 더하면 안 된다. |
| 규모 | 플랫폼 저장소 커밋 295개(9/19~9/28) 중 보안 규칙 파일을 건드린 것 15개 · 게이트웨이 42개 · 시험 폴더 127개. 파이스 저장소는 134개. 보안 규칙 122줄 → 464줄, 시험 폴더 파일 8개 → 38개. |
| 다룬 도구 | Firestore 보안 규칙 · 규칙 시험대(에뮬레이터) · Firebase CLI · Aside 브라우저 · 서비스 계정 키(읽기용·쓰기용 — 내용은 쓰지 않음) · 서비스워커 · Cloudflare Workers · Workers Builds · wrangler |
| 결과 | 규칙 시험 0 → 186개(전부 통과) · 게이트웨이 시험 47 → 192개 · DB 문서 15,310 → 5,831건 · 읽기 한도가 찬 날은 다섯 번(9/19·9/20·9/23·9/24·9/26)이었고 원인마다 막았다 |
| 공개 범위 | 사내(로그인 뒤) — 🔒 항목은 넣지 않음. 보안 이야기는 "무엇이 열려 있었고 어떻게 막았나"까지만 쓴다 |
한 줄 요약 — 무료 한도는 세어서 지키고, 규칙은 시험하고 올리고, 구멍은 일반 직원의 눈으로 찾는다. 사고의 원인은 대부분 직원이 아니라 만드는 쪽의 습관이었다.
13단계 · 플랫폼 지키기 — 한도 · 규칙 · 보안
목적
회사 플랫폼(SJ FLOW)은 Firestore 무료(Spark) 요금제 위에서 돈다. 하루 문서 읽기 5만 건을 넘으면 그날 내내 새 자료가 안 온다. 12단계에서 메신저와 AI 를 직원에게 열 준비를 하는 동안, 첫 사고가 9/19 새벽에 났다. 이 단계는 그 사고를 시작으로 (1) 한도를 알고 지키는 장치, (2) 규칙을 안전하게 바꾸는 방법, (3) 최고 관리자가 아닌 일반 직원의 눈으로 본 구멍을 막은 기록이다.
만든 순서
1 · 하루 5만 읽기 — 태운 건 직원이 아니라 만드는 쪽이었다 (9/19~9/28)
Firestore 무료는 하루 문서 읽기 5만 건이고 넘으면 429 가 난다. 리셋 시각은 한국 시각 17시다 — 처음엔 16시로 알았다가 콘솔 사용량 화면에서 "9/20 17:00 ~ 9/21 16:00"으로 표시된 것을 보고 바로잡았다(태평양 표준시 자정이라 서머타임을 안 따라간다). 한도가 찬 날은 다섯 번이었다.
- 9/19 새벽 — 야간 백업을 처음 돌렸다. 전 컬렉션의 모든 문서를 읽는 방식이라 한 번에 5만을 태웠다. 크론(정해진 시각에 도는 작업)이 매일 아침 9시라 그대로 두면 매일 아침 플랫폼이 멈출 판이어서 01:25 에 바로 껐다. 같은 날 오전, 한도가 찬 채로 직원 자리에서 플랫폼이 "불러오는 중"에서 멈췄다. 한도가 차면
getDocs(여러 문서를 한 번에 읽는 호출)가 오류가 아니라 영영 안 끝나는데(Firebase 라이브러리가 조용히 재시도한다).catch는 거부만 잡고 멈춤은 못 잡았다. 서버 조회에 7초 제한을 걸고 넘으면 캐시로 들어가며 띠로 알리게 했다. - 9/20 오후 — 같은 메시지 3,555건을 네 번 읽어 또 5만을 넘겼다(콘솔이 보여 준 한도일 9/20 17:00~9/21 16:00 의 읽기는 54,286건). 스크립트마다 예산이 있어도 실행을 넘나드는 낭비는 못 잡는다. 파이스에 하루 장부를 만들었다(3만을 넘으면 스스로 멈춘다).
- 9/21 — "일요일에 사람도 없는데 한도가 찼다는 게 말이 되냐"는 물음에 처음 한 설명이 틀렸다. "메신저 부팅 600읽기 × 사람 × 새로고침 ≈ 35,000"이라고 5만에 맞춰 산수를 지어냈던 것이다. 콘솔을 열어 보니 읽기 54,286 · 쓰기 185 · 삭제 24 였다 — 사람 몫은 한도에 붙어 있지 않았다. 근거 없이 그럴듯한 수를 만들지 말 것. 콘솔을 열면 10초에 끝난다.
- 9/23 — 처음으로 다 세어 봤다. DB 전체 15,310건, 본체 부팅 213건(적혀 있던 짐작은 2,000), 메신저 부팅 최대 600건(계량기가 없어 하루 읽기를 네 배 적게 보고 있었다). 찬 접속 한 번이 813건이므로 파이스 몫 3,000건을 뺀 4.7만을 채우려면 찬 접속 57번이 필요한데, 기억 노트는 어제·오늘 화면 확인하느라 캐시를 비우고 새로 연 횟수가 그만큼이라고 적고 있다. 이날 읽기 장부(b94)를 만들며 계량기가 캐시에서 온 것까지 세는 오류도 잡았다 — 자가 틀리면 없느니만 못하다. 앞 커밋의 진단도 정정했다("관리 화면이 한 클릭에 9,969건"은 틀렸고 실측 480건이다. 서버가 30일 범위만 거른다).
- 9/24 10:45 — 반쪽 난 백업 둘을 채우려고 백업을 한 번 더 완주시켰더니 또 5만이 찼다. 장부는 15,318건인데 아침에 요청 한도로 죽은 실행 둘이 각각 약 1만 건을 읽고 장부에 안 올린 채 죽어 있었다. 한도 가까이 가는 일을 하기 전에, 장부 합이 아니라 장부에 안 잡혔을 수 있는 읽기부터 센다. 죽은 실행은 읽고 죽는다.
- 9/26 08:21 — 새벽에 429. 장부로는 4만 건이 안 맞아 코드를 네 갈래로 훑었지만 못 박았다. Firebase 콘솔 사용량이 답이었다: 24시간 읽기 16만, 새벽 2시대 한 시간 2.2만, 그 시간 쓰기 0, 스냅샷 리스너 48개(플랫폼 탭 하나가 16개). 검증하고 안 닫은 브라우저 탭 셋이 뒤에서 연결을 끊었다 이었다를 되풀이하며 구독을 다시 걸고 있었다. 계량기는 캐시와 같으면 0 으로 세므로 아무도 몰랐다. 30분 넘게 가려진 탭은 Firestore 연결을 쉬게 했고(b110), 같은 문제를 가진 도구 화면 16개와 한 번 조회(getDoc)도 계량하게 했다.
결정도 이 단계에서 났다. 9/21 에 Blaze(쓴 만큼 내는 요금제)는 지출 상한이 없어서 Spark 를 유지하기로 했다 — 장부·백업·이관으로 실수로 태울 길이 줄었다. 9/28 에는 "맥을 서버로 쓰면 한도가 풀리지 않나"를 따져 안 하기로 했다(실시간 화면 18개와 Firestore 를 부르는 약 380곳, 규칙 시험을 모두 다시 짜야 하고, 맥이 멈추면 플랫폼 전체가 멈춘다). 지금까지 사고는 전부 낭비였다. 실제 쓰임은 작다.
2 · 무거운 것을 DB 밖으로 — 첨부 이관과 옛 기록 떼기 (9/19~9/25)
첨부를 700KB 조각으로 잘라 chunk__* 문서로 컬렉션 안에 넣고 있었다. 컬렉션을 한 번 훑으면 하루 5만이 통째로 날아가는 구조였다. 9/19 11:50 에 새 첨부를 게이트웨이의 파일 저장소(R2)로 올리고 DB 에는 열쇠 한 줄만 남기게 했다. 게이트웨이에는 /file/* 길과 버킷이 이미 있었다 — 만들어만 놓고 한 번도 안 쓰던 길이었다. 일부러 넣은 것 둘: 올리기가 실패하면 옛 조각 방식으로 물러서기(회사에는 게이트웨이 주소가 막힌 PC 가 있다), 올린 직후 되읽어 보기("올라간 줄 알았는데 못 읽는" 건 물러설 데가 없다).
9/20 에 옛 첨부 1,135개를 전부 R2 로 옮겼다. 조각 문서 1,535개를 지웠고 DB 문서는 16,390 → 14,855건이 됐다. 이관 중에 사고를 냈다. 한 기록에 첨부가 여럿이면 처음 떠 둔 원래 값 위에 하나씩 얹다가 앞서 바꾼 것이 되돌아가 102건이 없는 것을 가리켰다. 로그는 "실패 0"이었다. R2 열쇠가 chunkId 로 정해지는 꼴이라 끊긴 참조에서 되계산해 전부 되살렸다. 지우기 전에 반드시 읽고-고치고-쓰기, 그리고 "실패 0"을 믿지 말고 결과물을 센다(옮긴 뒤 참조 147/147, 고아 988/988 되읽기).
9/24 20:49 에는 DB 의 62%를 차지하던 옛 작업 기록(activityLog)의 8/25 이전 9,489건을 떼어 냈다. 지우기 전에 세 사본(게이트웨이 직접 읽기·맥 거울·R2 스냅샷)과 한 건씩 대조해 후보 전부가 있고 필드가 같은 것을 확인했고, 영구 보관본을 세 곳(맥·R2·드라이브)에 두었다. DB 는 15,310 → 5,831건이 됐다. 통째로 읽는 모든 작업이 2.6배 가벼워졌다. 이튿날 같은 일을 매달 1일 자동으로 하게 했다(보관 셋과 건수 대조가 끝난 뒤에만 지운다).
3 · 사본 셋과 열쇠 둘 — 백업은 영영 읽기 전용 (9/19~9/25)
백업·복원 스크립트(9/19 06:10)는 이렇게 설계했다. 읽기 예산을 먼저 정하고 넘으면 스스로 멈춘다 · Firestore 문서를 타입 그대로 저장한다(사람이 읽기 좋게 바꾸면 정수·시각이 문자열로 뭉개져 되살렸을 때 조용히 틀린다) · 삭제 반영은 전체를 끝까지 읽었을 때만 한다(중간에 끊긴 결과로 지우면 멀쩡한 문서가 백업에서 사라진다) · 복원은 기본이 연습이고 --진짜 일 때만 쓴다.
서비스 계정 열쇠를 둘로 갈랐다(9/20). 백업·동기화는 읽기 전용 열쇠로, 이관·복원은 쓰기 열쇠로만 한다. 이관이 막힌 자리가 정확히 읽기 열쇠로 쓰다 난 403 이었다. 쓰기 열쇠가 없으면 읽기 열쇠로 조용히 넘어가지 않고 던진다 — 조용히 넘어가면 "됐나 보다" 하다 지울 때 사고가 난다. 백업의 자체검사가 자기 소스를 읽어 쓰기 열쇠를 쓰면 실패시킨다(찾을 말을 쪼개서 이어 붙이지 않으면 검사문 자신이 걸려 늘 실패했다). 야간 무인 배치가 운영 데이터를 쓸 수 있게 되는 건 사람 눈에 안 보이는 종류라 코드로 막았다. 되살리기도 증명했다: 시험 컬렉션을 만들고 → 백업 → 지우고 → 되살리고 → 필드 하나하나 대조했다.
사본은 셋이 됐다. 맥이 매일 17:20 에 만드는 스냅샷, 구글 드라이브, 그리고 R2. 거기까지 오는 길에 오진이 하나 있었다. 9/24 아침 커밋 제목이 "회사 자료에 사본이 없었다"였는데 틀린 말이었다 — 맥 백업이 9/20 부터 매일 돌고 있었다. 게이트웨이의 R2 백업이 한 번도 안 돈 것을 보고 "백업이 없다"고 결론 내렸는데 기억 문서를 안 읽었다. 같은 날 바로잡았다(e4a10ec). R2 백업이 안 돈 진짜 이유는 무료 플랜이 한 실행에 하위 요청 50개까지만 허락하는데 코드의 900은 유료 숫자였던 것이다. 이어 받기로 고쳤지만 완주가 또 5만을 태웠고(위 9/24), 결국 R2 사본은 맥이 올리게 바꿨다(9/24 19:37) — 백업 때문에 Firestore 를 읽는 일이 없어졌다. "크론이 돌았나"를 가를 기록이 없어 심장박동 한 줄(_cron.json)을 남기게 했다.
4 · 강제 장치 셋 (9/20)
사람 눈에 안 보이는 사고를 코드로 막은 것 셋이 같은 날 들어갔다.
- AI 검색 범위. 그때까지 AI 비서는 색인 전체에서 찾았고 막던 것은 지침 한 줄("범위 밖 자료는 주어지지 않는다")뿐이었다 — 약속이지 통제가 아니다. 이제 범위는 검증된 로그인 토큰에서만 뽑고(요청 몸의 범위는 읽지 않는다), 못 구하면
전사만 준다. 맥과 클라우드 색인 양쪽에 같은 잣대를 걸고, 후보를 뽑는 단계에서 거른다(뽑아 놓고 빼면 이미 모델에 들어간 뒤다).비밀범위는 어떤 경우에도 AI 에게 안 준다. - 품질기록 판(rev)과 변경 이력. NCR·CAR(부적합 보고서·시정조치 요구서)을 고치면 그냥 덮어쓰고 있었다. 외부 심사원이 "언제 누가 어떻게 바꿨나"를 물으면 답할 자료가 없었다. 규칙이 저장할 때
rev가 반드시 커지게 강제하고(옛 기록은 0 으로 읽어 따로 채울 필요가 없다), 이력 컬렉션은 덧붙이기 전용이다(수정·삭제가if false). 사진·첨부 본문은 이력에 안 담는다 — 담으면 두 번째 9/19 가 된다. - 하루 읽기 장부. 1번 기록.
게시 직후 배포된 운영 규칙에 진짜 로그인 토큰으로 부딪쳐 보는 시험도 만들었다(10/10 통과). 시험대는 규칙 파일을 보지만 콘솔에 붙인 것이 그 파일과 같다는 보장은 없기 때문이다.
5 · 규칙 시험대와 올리는 순서 (9/20~9/28)
그전까지는 보안 규칙을 시험대 없이 운영에 바로 올리고 있었다. 9/20 20:33 에 에뮬레이터에 규칙 파일을 걸고 실제로 읽고 써 보는 시험대를 만들었다(처음 37개). 규칙 시험은 9/28 까지 186개가 됐고 직접 돌려 186개 모두 통과했다.
순서가 규칙이다. 규칙은 걸러 주는 체가 아니라 질의 전체를 심사한다. 규칙 안에 있다는 것을 증명하지 못하는 질의는 문서 단위로 걸러지지 않고 통째로 거부된다. 그래서 질의를 좁히는 변경은 코드 먼저, 규칙 나중이다. 좁힌 코드와 넓은 옛 규칙은 안전하지만 옛 코드와 좁힌 새 규칙은 멎는다.
- 9/21 메신저 2단계(readers 에 든 사람만 그 메시지를 받는다). 배포 순서를 글로 적어 놓고 b53 을 먼저 푸시해 버린 것을 바로 깨달았다. 17시에 한도가 풀리는 순간 전 직원이 빈 메신저를 볼 참이었다(복합 색인도 옛 메시지의 readers 도 아직 없었다). 코드는 배포돼 있되 동작은 옛길로 두는 스위치를 뒀다가(b54), 백필·색인·규칙이 끝난 뒤 한 줄을 켰다(b55). 되돌릴 수 없는 순서를 글로 적었으면 그 글대로 하기 전에 푸시하지 않는다.
- 반대로 새 컬렉션을 쓰는 변경(9/28 결재 서명)은 새 화면이 쓰는데 옛 규칙엔 그 규칙이 없어 거부되므로 규칙 먼저였다.
- 새 시험은 옛 규칙으로도 돌려 실패하는지 확인한다. 9/26 에 이 방법으로 구멍이 진짜라는 증거를 얻었다(옛 규칙에서 새 시험 9개가 실패, 앞 시험이 품질원을 실제로 재무부 소속·최고 관리자로 만들어 뒤 시험 둘까지 연달아 실패했다).
- 규칙 시험 수: 37(9/20) → 50 → 54(9/21) → 60(9/26 낮) → 97(9/26 저녁) → 116(9/26 밤) → 135 → 161(9/27) → 186(9/28).
- 규칙 언어는 한글 필드 이름·함수 이름을 못 읽는다(컴파일이 죽는다). 필드는 영문으로 쓰고 한글은 문자열 안에서만 쓴다. 시험대가 이 실수를 운영에 올리기 전에 잡았다.
- Firebase CLI(명령줄 도구) 로그인이 죽으면 Aside 브라우저(AI 가 직접 조작하는 브라우저)로 콘솔 편집기를 열어 붙여 넣는다. 붙인 뒤 전문을 글자 단위로 대조하고 게시한 뒤에는 맨 위 판의 전문을 다시 대조한다 — 9/26 에 게시 단추가 눌렸는데 직전 판과 같은 글이 올라간 적이 있다.
6 · 고친 게 안 닿던 이유 — 서비스워커와 판 번호 (9/21~9/26)
"어제 고친 게 그대로다"라는 제보를 코드로 파다가 코드도 배포본도 멀쩡한 것을 알았다. 화면만 옛것이었다. 서비스워커(앱 껍데기를 기기에 저장해 두는 프로그램)가 주소 끝 ?v=(판 번호)를 떼고 옛 파일을 찾아 캐시를 먼저 내주고 있었다. 판 번호를 손으로 일곱 군데 올려도 한 글자도 안 갔다. 같은 출처는 그물(네트워크) 먼저, 끊기면 캐시로 바꿨다(9/21 08:17).
그 뒤에도 같은 종류가 둘 더 있었다. 9/22 에는 열 판을 올리는 동안 index.html 의 판 번호가 b67 에 멈춰 있어 iframe(화면 안에 끼운 다른 화면) 주소가 영영 같았다. 강제 새로고침을 네 번 해도 소용없었고 서비스워커 등록 해제와 캐시 삭제와 ?fresh= 를 모두 해야 새 판이 왔다. 판 번호가 네 곳에 적혀 있다는 점이 문제여서 npm run build 가 네 곳이 같은 값인지 대조하게 했다. 9/26 에는 새 모듈을 import 하고 서비스워커의 껍데기 목록에는 안 넣어서, 온라인에서는 멀쩡하고 비행기 모드에서만 앱이 안 뜨는 것이 나왔다. 시험이 import 를 따라가 껍데기 목록과 대조한다.
고친 것이 사람에게 안 닿으면 안 고친 것이다. "고쳤는데 그대로다"를 들으면 코드보다 먼저 배포본을 받아 보고, 사용자 주소창의 ?v= 가 몇인지 본다. 둘이 다르면 전달의 문제다.
7 · 충돌 해소와 push 를 잇지 마라 (9/26)
9/26 오후, 다른 세션이 같은 시간에 main 에 올려 index.html 의 판 번호 줄에서 rebase 충돌이 났다. 해소 스크립트가 셸의 역슬래시 함정으로 실패했는데, 같은 명령 묶음의 다음 줄 git add · rebase --continue · push 가 그대로 돌아 충돌 표시(<<<<<<<)가 남은 index.html 이 올라갔다. npm test 는 그 줄을 안 봐서 통과했다. 그때는 push 가 곧 직원 화면이었다. 깨진 파일이 직원 화면에 있던 시간을 기억 노트는 3~4분이라 적었다. 충돌 표시가 든 커밋과 그것을 걷어낸 커밋의 시각 차이는 1분 32초다(올라간 시각은 git 에 없어 모른다).
- 충돌을 해소하고 → 충돌 표시가 0 인지 확인하고 → 그다음에 따로 add/continue/push 한다. 한 명령에 heredoc(여러 줄 글을 명령 안에 바로 넣는 셸 문법)과 push 를 줄로 잇지 않는다(앞이 실패해도 다음 줄이 멈추지 않는다).
- 시험이 이제
index.html의 충돌 표시를 잡는다. - 9/28 에는 두 번째 꼴이 있었다.
npm test가 종료 코드 1 로 끝난 것이 찍혔는데 다음 줄이 그대로 push 했다. 실패 자체는 다른 사람 커밋 때문이었지만 그 명령이 멈췄어야 했다. push 앞에 시험 결과로 멈추는 줄을 둔다.
8 · 일반 직원 눈으로 — 보안 전수 대조 (9/26~9/27)
9/26 하루에 "누구나 쓰는 값을 남이 믿는" 꼴의 구멍이 다섯 번 나왔다(자기 users 문서의 부서·등급, 경비, 영수증 파일, AI 공용 설정, 가입). 전부 최고 관리자 계정으로만 시험해서 안 보이던 것들이었다. 그래서 한 번에 훑었다. 일반 직원의 눈으로 네 갈래(규칙 · 메신저 · 파일 저장소 · 파이스의 인터넷 문)를 읽기만 하며 조사하고, 확인된 것을 고쳤다.
무엇이 열려 있었고 어떻게 막았나(재현 방법은 적지 않는다).
- 직원이 쓴 글이 화면에 그대로 들어가던 자리(저장형 XSS). 일정·업무·결재·NCR·회의록 같은 직원 입력 칸을 화면 문자열에 그대로 넣고 있었다. 같은 출처의 화면이라 남의 글이 대표 권한 화면에서도 실행될 수 있는 구조였다. 이스케이프(특수 글자를 명령이 아닌 글자 그대로 보이게 바꾸기)·주소 거르기·onclick 인자 처리로 막았다. 본체
index.html에서 이스케이프 도우미_escNote(가 나오는 곳이 8곳에서 316곳으로, 도구 화면에서는jsArg(가 0곳에서 87곳으로 늘었고(함수 정의 줄까지 센 값), 시험 두 벌이 옛 모양이 다시 생기지 않게 지킨다. - 메신저 방 문서. 메시지를 누가 받는지를 방 문서가 정하는데 방 문서를 사내 누구나 만들고 고칠 수 있었다. 또 마지막 말 미리보기가 모든 직원 브라우저로 내려가고 있었다. 규칙이 방 만들기 모양과 고칠 수 있는 칸을 제한하고 미리보기는 쓰지 않으며 메시지 칸을 허용 목록으로 제한한다.
- 파일 저장소. 로그인만 확인하고 아무 열쇠나 서명하거나 덮어쓰던 것을, 직원은 새 파일만 올리고(이미 있으면 거절) 내려받을 때 형식을 거르게 했다.
- 규칙의 빈 곳. 경비 설정·내역(재무부만), 자산·라이선스(총무·임원만), 결재선 기억(본인만), users 본인 수정(두 칸만), 가입(스스로 가입은 신청으로만, 관리자 승인 뒤 반영).
- 파이스의 인터넷 문. 토큰이 비어 있으면 열리던 것과 옛 화면 캡처 보기 주소를 닫거나 지웠다.
- NCR·CAR 지우기. 사내 누구나 영영 지울 수 있던 것을 휴지통으로 옮기는 방식으로 바꿨다. 원본 삭제는 같은 일괄 쓰기에서 똑같은 사본이 휴지통에 생길 때만 허용하고, 되살리기·비우기는 관리자만 한다.
- 조용히 죽은 알림 넷. 규칙을 조이자
readers없는 메시지는 아무도 못 읽게 돼, 🤖 배지·알림 종, 관문의 아침 알림, 검교정 알림, 옛 비서의 메시지 보내기 넷이 조용히 멈춰 있었다. 받는 사람을 박고 방 단위로 세게 고쳤다. 새 메시지를 쓰는 곳은readers를 반드시 박는다. - 거부 감시. 규칙에 막힌 쓰기를 오류 기록에 남기고(9/27 새벽) 맥에서 시각 뒤로만 읽어 센다. 9/27 00:08 이후 기록은 0건이다.
규칙 시험은 9/26 저녁 97개에서 9/27 오전 172개가 됐다. 같은 시험을 옛 규칙으로 돌리면 가입 11개·메신저 14개·휴지통 11개·경비 11개가 뚫리는 것으로 구멍이 진짜였음을 확인했다.
9 · 저장소 비공개와 사이트 이사 (9/27)
9/27 오전 제3자 대조가 저장소가 공개라는 것을 잡았다. GitHub Pages(저장소 파일을 그대로 웹사이트로 내 주는 기능)가 저장소 전부를 인터넷에 내고 있었다 — 아침 메일 정리 파일(직원 셋의 메일 요약), 대시보드 31장, 운영 인수인계 문서, 게이트웨이 코드까지 몇 주째 누구나 열 수 있었다. 결정은 비공개였다. 문제는 조직이 무료 요금제라 비공개 저장소는 Pages 를 못 쓴다는 점이었다. 비공개만 누르면 사이트가 즉시 꺼진다. 호스트를 먼저 옮기고 그다음 비공개로 돌렸다.
- 사이트를 Cloudflare 워커(Cloudflare 위에서 도는 작은 서버 프로그램)로 옮겼다. 정적 파일은 워커를 안 거쳐 무료 요청 한도를 안 쓰고,
/data/*만 워커가 먼저 받아 로그인 토큰을 검사해 본인 이메일 몫만 내준다(최고 관리자도 남의 것은 못 본다). 파일 65개를 바이트 단위로 대조해 같은 것을 확인한 뒤 옮겼다. build.mjs는 작업 폴더가 아니라 커밋(HEAD)에서 허용 목록만 내보낸다. 작업 폴더를 올리면 커밋 금지인 재료 표가 인터넷에 나간다.- 푸시하면 Workers Builds 가 1~2분 뒤 자동 배포한다. GitHub 앱은 이 저장소 하나에만 권한을 줬다. 빌드 명령 칸의 기본값이 우리
package.json에서는 판 번호 시험이라 사이트가 안 만들어져서node site/build.mjs로 바꿨다. DNS(주소 이름을 서버로 잇는 표) 교체는 명령줄 도구로 안 돼서 대시보드 화면에서 했다. - 청사진·로드맵 문서(
docs/)는 비공개 저장소가 돼도 주소만 알면 열려서 회사 계정 로그인 뒤로 옮겼다(12시간 서명 쿠키). 같은 날 저녁 차량 앱 사이트도 워커로 옮기고 비공개로 돌렸다. 공개로 남긴 것은 개인 프로젝트 셋이다. - 함정: 게이트웨이를 배포할 때
--config wrangler.toml을 안 붙이면 맨 위 설정 파일(사이트)이 먼저 잡혀 사이트가 대신 올라간다(9/27 에 실제로 그랬다).
10 · 결재 서명 위조 막기 · NCR·CAR "즉시 반영 안 됨" (9/28)
NCR·CAR. 품질관리부가 "조회와 즉시 반영이 제대로 안 된다"고 했다. 추측 없이 이력 기록을 실측했다. 한 사람이 저장 한 번을 할 때마다 NCR 7~13건이 같은 초에 다시 써지고 있었다(판 3 → 7). 원인 셋이었다. ① 바뀐 것을 JSON.stringify 글자 비교로 가르니 칸 순서만 달라도 "바뀜"이었다(같은 비교가 네 화면에). ② 저장이 끝난 뒤 기준선을 저장 전 배열로 덮어, 그사이 온 새 스냅숏이 옛것으로 되돌려졌다 — 이후 저장마다 전체를 다시 쓴다. ③ 판(rev)을 옛 기준선에서 세어 서버 판보다 안 커지면 규칙이 거부했다 — 그 사람의 변경이 사라지고 「클라우드 저장 실패」가 떴다. 아니었던 것도 확인했다: 읽기 한도(장부 1,923/5만)·읽기 규칙·오류 기록. 고침은 키를 정렬해 비교하고(칸 순서만 다른 것은 안 쓴다) 기다리는 사이 스냅숏이 오면 자기가 쓴 것만 갈아 끼우는 것이다. 라이브에서 칸 순서를 섞은 23건이 옛 비교로는 23건 재쓰기, 새 비교로는 0건이었다. 시험 16개를 붙였다.
결재 서명. 결재 문서 규칙이 사내 누구나 쓰기였고, ITP(검사·시험 계획서) 표지·교정 견적의뢰서·결재 도장표는 결재 문서의 상태와 결재선을 그대로 믿고 사용자의 서명 그림을 찍었다. 지어낸 결재 문서에도 남의 실제 서명이 찍힐 수 있었다. 서명 그림은 이제 결재자 본인만 쓸 수 있는 서명 기록만 믿는다. 규칙이 서명한 사람과 결재선의 그 단계가 맞는지 대조하고, 서명 기록은 고치거나 지울 수 없다. 이번에는 새 컬렉션을 쓰는 변경이라 규칙을 먼저 올렸다. 옛 결재 9건의 서명은 따로 채워 두었다. 배포 뒤에 결재선을 보고 채우면 위조를 인정하는 꼴이라 한 번만 했다. 곁가지로 ITP 아이템 id 가 문서에서는 숫자이고 결재·서명에서는 글자라 재승인(Rev 1)이 표지에서 빠지던 것, 측정기구 화면에서 견적의뢰서 「문서 보기」가 처음부터 실패하던 것도 고쳤다.
💡 설명
- Firestore · Spark 요금제 · 429: 회사 플랫폼의 데이터베이스(Google Firebase). 무료 요금제는 하루 읽기 5만·쓰기 2만 건이고, 넘으면 다음 리셋까지 멈추며 429("한도를 다 썼다"는 응답)를 낸다. 429 는 고장이 아니라 한도 신호다.
- 영속 캐시: 한 번 받은 자료를 브라우저에 저장해 두고 다음 접속에서 바뀐 것만 받게 하는 장치. 읽기 건수를 줄인다.
- 스냅샷 리스너(구독): 데이터가 바뀌면 화면에 즉시 알려 주도록 걸어 둔 연결. 탭 하나가 여러 개를 건다.
- 보안 규칙: Firestore 가 읽고 쓰는 사람을 DB 쪽에서 검사하는 규칙 파일. 화면이 숨겨 주는 것과 달리 개발자 도구로도 못 넘는다.
- 에뮬레이터 시험대: 개발 컴퓨터에 가짜 Firestore 를 띄우고 규칙을 걸어 실제로 읽고 써 보는 시험. 운영 데이터는 안 건드린다.
- 서비스워커 · 캐시버스터(?v=): 서비스워커는 앱 껍데기를 기기에 저장해 두는 프로그램, 캐시버스터는 주소 끝에 판 번호를 붙여 옛 파일을 못 쓰게 하는 방법이다.
- 저장형 XSS: 직원이 입력해 저장된 글 속의 명령이 이스케이프 없이 화면에 들어가, 그 화면을 연 다른 사람의 브라우저에서 실행되는 문제.
- 서비스 계정 키(읽기용·쓰기용): 사람이 아니라 프로그램이 DB 에 접속하는 열쇠. 읽기만 되는 것과 쓰기까지 되는 것을 따로 둔다.
- 스냅샷(백업 사본): 어느 날의 DB 전체를 파일로 떠 둔 사본.
- rebase · 충돌 표시: 남이 먼저 올린 변경 위에 자기 변경을 다시 얹는 작업(rebase)에서 같은 줄이 겹치면 파일에
<<<<<<<표시가 남는다. 지우지 않고 올리면 깨진 파일이 나간다. - Workers Builds: 저장소에 올리면 Cloudflare 가 알아서 사이트를 만들어 내보내 주는 자동 배포.
- 휴지통 컬렉션: 지울 기록을 진짜로 지우지 않고 옮겨 두는 보관함.
함정과 배운 것
- 한도를 태운 건 대부분 만드는 쪽의 습관이었다. 화면 확인은 캐시를 남긴 채 하고, 시험은 에뮬레이터로 한다. 검증하려고 연 브라우저 탭은 끝나면 비운다. 배치는 돌리기 전에 먼저 센다(집계 질의는 1,000건당 읽기 1건이라 거의 공짜다).
- 근거 없이 그럴듯한 수를 만들지 않는다. "600 × 사람 × 새로고침 ≈ 35,000"은 산수만 맞았고 실제와 달랐다. 콘솔을 열면 10초에 끝난다.
- 자(계량기)가 틀리면 없느니만 못하다. 계량기가 캐시를 세어 몇 배 크게 나왔고, 다른 계량기는 캐시와 같으면 0 이라 새는 걸 못 봤다. 장부는 아래로 센 값이고 판단은 콘솔로 한다.
- 죽은 실행은 읽고 죽는다. 장부 합만 보고 한도 가까이 가는 일을 하지 않는다.
- 한도가 차면 요청이 실패하는 게 아니라 안 끝난다. 제한 시간과 캐시 대체가 필요하다.
- 규칙은 코드 먼저, 규칙 나중. 단 새 컬렉션을 쓰는 변경은 반대다. 되돌릴 수 없는 순서를 적었으면 그 글대로 하기 전에 푸시하지 않는다.
- 새 규칙 시험은 옛 규칙으로 돌려 실패하는지 본다. 통과만 보면 시험이 아무것도 안 보고 있을 수 있다.
- 고친 것이 사람에게 안 닿으면 안 고친 것이다. 서비스워커·판 번호·껍데기 목록 세 갈래가 모두 "코드는 멀쩡한데 화면이 옛것"으로 나타났다.
- 충돌 해소와 push 를 한 명령으로 잇지 않는다. 앞이 실패해도 다음 줄이 멈추지 않는다. push 앞에는 시험 결과로 멈추는 줄을 둔다. (같은 함정이 이후 10/2 에 한 번 더 났다.)
- 한 계정(최고 관리자)으로만 시험하면 권한 구멍이 안 보인다. 일반 직원 계정으로 훑고, 규칙을 조인 뒤엔 읽을 사람이 없어 조용히 멈춘 것이 없는지 본다.
- "백업이 없다"고 결론 내리기 전에 기억 문서부터 읽는다. 있는 것을 없다고 보고했다.
- 새 메시지를 쓰는 곳은 받는 사람(readers)을 반드시 박는다. 안 박으면 아무도 못 본다.
출처
- 플랫폼 저장소(sejong-platform-v2) 커밋: f6601ea · 7288bf9 · 73f544c · 829aa64 · 618a1f2 · 9a4c5e1 · 74c1008 · d654ce3 · d4fa770 · 16bd0f0 · 89d9c68 · 098d98d · c91ad6c · dda2e02 · 4f1a078 · 643ebbf · 901d30d · d7a6dad · 988f155 · 8ebedd5 · cb7bf94 · e4a10ec · f63bef6 · be34272 · da44e2e · d66b6b7 · 19f1c27 · 5d03abb · fbcf45d · 39c669a · 511f68c · 0f890fa · 13ae5a3 · 5e9fd10 · 92edc7c · 9f92273 · 3d01ce1 · fa63630 · 3adf3ad · 616b331 · c8f1ee9 · e5e6218 · 89cc112 · 31df174 · c8f41c9 · 541304b · 19e8092 · 25a0074 · 695748c
- 파이스 저장소 커밋: 328ed6a · 80ab604 · 970e5e4 · 58ac346 · a6df157 · 30e09a1 · d224985 · 3bf8f4f · 8a1ce2f
- 기억 노트: sejong-platform-limits · platform-guardrails · platform-rules-testing · platform-write-key · chunk-migration-lesson · firestore-rules-publish · sw-cache-buster-trap · merge-push-trap · security-audit-0927 · approval-sign-records · ncr-car-sync-rewrite · public-repo-exposure
- 문서: 플랫폼
운영-인수인계.md·PLATFORM-STORAGE-PLAN.md·test/rules-README.md
← 12 SJ 메신저와 AI 비서 — 직원 모두의 창구 · 14 사무직 AI 작업실 — 직원마다 개인 비서 →