운영이 데이터베이스 암호를 동료에게 줄 때 가장 편한 쓰기는 클릭할 수 있는 링크입니다. 문제는 그 링크가 한 번 열리면 브라우저, 리버스 프록시, 오리진, 여러 로그를 탄다는 점입니다. 진짜 통제할 것은 「암호화가 안전해 보이는가」가 아니라 복호화 키가 어느 층에 나타나는가입니다.
키를 ?key= 뒤에 쓰면 HTTP 요청의 일부가 되어 거의 반드시 access log에 들어갑니다. 키를 # 뒤에 쓰면 브라우저는 보내기 전에 그걸 뗍니다. 어떤 제품의 약속이 아니라 URI 규격의 fragment 처리입니다. 이 글이 답하는 것은, 키를 왜 우물정 뒤에 두어야 하는지, 그리고 「서버가 그 한 줄을 받지 않았다」를 어떻게 직접 보는지입니다.
키를 쿼리에 넣으면 로그에 무엇이 남나
쿼리(?부터 # 앞까지)는 요청 대상입니다. 브라우저가 /s.html?id=abc&key=MYSECRET를 받을 때 경로와 쿼리는 회선에 탑니다. 오리진도, 앞단 Nginx, CDN, WAF, APM도 봅니다. 기본 access log는 request URI를 담는 경우가 많아 key=MYSECRET이 상태 코드, 소요 시간과 같은 줄에 쓰입니다.
이 한 줄은 나중에 지우기 어렵습니다. 로그는 디스크로 돌고 백업, SIEM, 「500을 복사」하는 지원 조사로 들어갑니다. 오류 추적은 요청 본문과 URL을 좋아합니다. 복호화가 한 번 실패하면 키가 든 주소가 크래시 보고에 들어갈 수 있습니다. 법역과 하청 운영은 「로그는 보지 않습니다」를 감사할 수 없는 문장으로 바꿉니다. 확인할 수 있는 것은 프로토콜이지 상대 전산실의 내부 규정이 아닙니다.
더 숨은 복사본도 있습니다. 브라우저 자신의 기록, 공유한 「전체 URL」 스크린샷, 감시 스크립트가 location.href를 통째로 올리는 일. 쿼리는 적어도 서버 쪽에 먼저 떨어집니다. fragment가 막는 것은 이 한 부이지 기기 위의 모든 복사본이 아닙니다. 먼저 서버 쪽을 막는 것이 한 번만 비밀을 전하는 일의 전제입니다.
키를 POST 폼에 넣어도 해결이 아닙니다. 만들기 API가 평문이나 키를 JSON에 쓰면 서버는 읽습니다. 바른 제약은 브라우저를 떠나는 것이 암호문 번호와 암호문뿐이라는 점입니다. 풀 수 있는 재료는 로그에 들어갈 어떤 칸에도 나오면 안 됩니다.
HTTPS는 상대가 쿼리를 읽는 것을 막지 못합니다
TLS가 막는 것은 경로상의 도청자입니다. 오리진이 TLS를 끝낸 뒤 받는 것은 복호화된 요청 줄입니다. 쿼리는 서버에게 평문입니다. fragment는 이 요청에 애초에 나타나지 않습니다.
# 뒤는 HTTP에서 어디로 가나
URI는 프로토콜, 호스트, 경로, 쿼리, 조각으로 나뉩니다. 조각(fragment)은 #로 시작하며 원래 페이지 안 앵커용입니다. 규격은 사용자 에이전트가 문서를 받을 때 조각을 빼고 보낸다고 합니다. HTTP 요청 줄에 #... 칸은 없고, 프록시와 오리진은 그걸 access log에 쓸 수 없습니다.
페이지가 실린 뒤 스크립트는 location.hash로 우물정 뒤를 읽습니다. 클라이언트 메모리의 문자열이지 서버가 돌려준 칸이 아닙니다. 읽기의 전형은 경로나 쿼리의 번호로 암호문을 받고, 기기에서 읽은 키로 AES-GCM 복호화하는 것입니다. 서버는 처음부터 끝까지 암호문만 만집니다. 키가 없으면 암호문은 쓸모가 없습니다.
MyPassGen 영지식 열람 후 폐기는 이 순서입니다. 브라우저에서 AES-256-GCM 암호화, 난수 256비트 키를 Base64URL로 # 뒤에 붙입니다. 올라가는 것은 암호문과 만료, 열람 횟수 같은 메타입니다. 읽기 페이지는 받는 사람에게 공개이며 상대의 선가입은 없습니다. 한 번만 비밀을 전하는데 동료 계정을 열게 하면 사슬은 이미 끊긴 겁니다. 첫 성공 열람 후 암호문은 설계대로 폐기됩니다. 제품 사실이지 「서버가 보지 않는다고 보장」이 아닙니다.
요청에서 보여야 할 것과 보이면 안 되는 것
만들 때 POST 본문에는 암호문이 있어야 하고, 방금 입력한 원문도 # 뒤에 나중에 나올 키도 없어야 합니다. 읽기 GET은 암호문 번호만. 개발자 도구의 요청 URL은 # 앞에서 멈춰야 합니다. 방금 만든 키 부분이 요청에 맞으면 구현이 키를 잘못 두었거나, 분석 스크립트가 전체 location.href를 보낸 것입니다.
| URL 구간 | 예 | 서버 / 프록시가 보나 | 키를 두어도 되나 |
|---|---|---|---|
| 경로 | /s.html 또는 /s/{id} |
보임. access log에 들어감 | 암호문 번호만. 키는 두지 않음 |
| 쿼리 | ?id=abc&key=… |
전체가 보임. 기본으로 로그에 | 맞지 않음. key=는 로그에 쓰는 것 |
| Fragment | # 뒤의 Base64URL 키 |
HTTP 요청에 이 구간이 없음 | 「브라우저만」의 키 통로로 맞음 |
| 요청 본문 | 만들 때 JSON | 오리진은 봄. 일부 프록시는 샘플링 | 암호문과 민감하지 않은 메타만 |
여기서 「영지식」은 좁습니다. 서버가 남겨도 풀 수 없다는 뜻입니다. 「아무것도 안 남긴다」도, 「전체 링크를 가진 사람이 못 푼다」도 아닙니다. 전체 링크는 가지면 복호화되는 증표입니다. # 뒤가 없으면 못 풀고, 한 통 더 복사하면 열쇠가 하나 늘어납니다.
Network 패널로 키가 회선에 탔는지 확인
구호는 스스로를 증명하지 못합니다. 아래 단계는 브라우저 개발자 도구에만 기대 「링크에 키가 있으니 서버가 봤다」와 「페이지에 로컬 암호화라고 썼으니 나가지 않았다」를 드러냅니다. 형식 증명이 아니라 이번 클릭이 키를 HTTP에 넣었는지 확인하는 것입니다.
우물정이 있는 테스트 링크를 준비.방금 만든 열람 후 폐기여도, 아무 기기 페이지에 #TESTKEY-확인전용을 붙인 것이어도 됩니다. 먼저 # 뒤를 메모장에 복사해 나중에 검색합니다.
Network를 열고 Preserve log를 켭니다.Chrome, Edge, Firefox 모두 됩니다. 필터는 먼저 Fetch/XHR과 문서, 그다음 전체. Preserve log는 이동으로 기록이 지워지는 것을 막습니다. 주소창에는 전체 URL이 보이지만 브라우저 표시이지 회선에 탄 내용이 아닙니다.
링크를 열거나 Enter로 이동합니다.요청 목록에서 문서와 이어지는 XHR을 엽니다. Request URL은 # 앞에서 멈춰야 합니다. 쿼리에 암호문 번호는 있어도 되고, 메모장의 키는 나오면 안 됩니다.
키 부분으로 패널 전체를 검색.Network 검색창에 키, 또는 정적 스크립트에 우연히 없을 부분을 붙입니다. 어떤 요청도 맞지 않으면 적어도 이번 이동은 fragment를 HTTP 내용으로 보내지 않은 것입니다.
잘못된 쓰기를 한 번 대조.같은 키를 ?key=로 옮겨 다시 엽니다(실제 유출이 없는 테스트 값으로). 이번 검색은 요청 URL에 맞아야 합니다. 두 결과를 나란히 두면 어떤 그림보다 분명합니다.
만들기 동작에서 원문을 다시 검색.열람 후 폐기 만들기 페이지에서 테스트용 가짜 암호를 보냈다면 그 원문으로 POST 본문을 검색합니다. 맞아야 하는 것은 암호문 칸의 형태이지 원문 자체가 아닙니다. 키 칸도 없어야 합니다. 읽기는 받는 사람에게 공개이며 선로그인은 없습니다.
이 단계가 증명하는 것과 못 하는 것
Network가 확인하는 것은 이번 조작이 키나 원문을 HTTP에 넣었는지입니다. 악성 확장, XSS로 끼워진 스크립트, 회사 SSL 복호화 프록시 뒤의 페이지는 보이지 않습니다. 「키가 쿼리에 있다」를 드러내기에는 충분합니다. 엔드포인트 보안 증명으로 이해하지 마세요.
Fragment는 로그를 막지만 링크 전체 전달은 막지 못함
키를 # 뒤에 두면 풀리는 것은 「오리진과 프록시가 기본으로 키를 읽는다」는 일입니다. 아래 경로는 남습니다. 기대에 적는 편이 알고리즘 세 글자를 찍는 것보다 쓸모 있습니다.
전체 링크는 가지면 복호화됨
경로와 fragment를 가진 사람은 폐기나 만료 전까지 읽기 페이지에서 암호문을 풉니다. 채팅 기록, 메일 보관, 브라우저 기록, 화면 공유, 어깨 너머는 HTTP 규격 밖입니다. 열람 후 폐기가 줄이는 것은 반복 열기와 서버가 평문을 오래 가지는 일이며, 상대의 스크린샷이나 재전달은 막지 못합니다.
Referer, 미리보기 카드, 분석 스크립트
요즘 브라우저는 Referer에 fragment를 넣지 않는 경우가 많습니다. 그러나 「페이지가 스스로 location.href를 분석 이벤트에 넣는」 구현은 구하지 못합니다. 만들기나 읽기가 전체 주소를 측정 속성으로 올리면 키는 클라이언트 통로로 새고, 서버 access log가 깨끗해도 소용 없습니다. 측정 요청도 같은 키로 다시 검색하세요.
메신저와 메일 미리보기는 # 뒤를 남긴다는 보장이 없습니다. 경로만 받는 크롤러도 있어 미리보기는 실패해도 키는 붙인 원문에 남습니다. 짧은 링크로 fragment를 떨어뜨리는 것도 있어 상대는 「링크가 불완전」만 봅니다. 보내기 전 시크릿 창에서 한 번 열어 읽기가 복호화되는지 확인하는 편이 「그대로 연다」를 믿는 것보다 확실합니다.
브라우저 기록과 로컬 스크립트
주소창의 전체 URL은 기록에 들어갑니다. 공용 PC나 기록을 동기화한 프로필은 만료 전까지 유효한 열쇠를 남기는 것과 같습니다. XSS나 가로채인 스크립트는 location.hash를 읽습니다. Web Crypto가 지키는 것은 정직한 페이지의 키 조작이지 「악성 스크립트가 이미 동일 출처에 들어온 뒤」가 아닙니다. 엔드포인트와 프론트 공급망 문제이며 「서버가 키를 가지면 안 된다」와 따로 다룹니다.
# 뒤에 남을 것과 서버로 나가도 되는 것
기기에 남기고 fragment로만 받는 사람 브라우저에 넘겨도 되는 것은 평문을 바로 되돌릴 재료, 곧 대칭 키 자체입니다. 쿼리나 요청 본문에 들어가는 순간 영지식 이야기는 끊깁니다.
기기를 떠나도 되는 것은 서버에 쓸모없는 데이터뿐입니다. 암호문 blob, 추측할 수 없는 번호, 만료, 최대 열람 횟수. MyPassGen 열람 후 폐기는 한 건 32 KB까지. 비밀번호, API 키 조각, 짧은 설명에 맞고 디스크 이미지 전체에는 맞지 않습니다. 큰 파일은 먼저 기기에서 .lock / .enc로 만든 뒤 다른 길로 보냅니다. 서버는 첫 성공 열람 후 암호문을 지우고, 만료된 미열람도 지웁니다. 서버 평문 복사본이 없어 링크를 잃어도 「지원에 복원」은 할 수 없습니다.
「페이지가 스크립트를 요청했다」와 「키를 올렸다」를 섞지 마세요. 정적 리소스는 정상입니다. 찾을 것은 키 부분이 요청 URL, Query, Payload, 사용자 헤더에 나왔는지입니다. 이 사이트도 키나 원문을 분석 이벤트 내용으로 보내지 않습니다. 이벤트에 비밀 자체는 없어야 합니다.
키를 동료에게 주고 서버 로그에 쓰고 싶지 않을 때
흔한 대안의 한계는 구체적입니다. 평문 메일과 메신저는 암호를 양쪽 기록, 서버 보관, 기기 백업에 남기고 만료되어도 상대 분은 지워지지 않습니다. 드라이브는 기기에서 먼저 암호화해 암호문을 올리지 않으면 서버가 읽습니다. 키를 쿼리에 두는 것은 리버스 프록시에게 메모를 부탁하는 것과 같습니다. 불편해서가 아니라 평문 창이 프로토콜에 쓰여 있습니다.
짧은 텍스트 비밀——root 암호, 복구 코드, 짧은 연결 문자열——에 필요한 것은 기기 암호화, 키가 HTTP에 안 탐, 받는 사람이 열면 보고, 보면 암호문이 사라지는 것입니다. MyPassGen 열람 후 폐기 링크는 이 제약으로 움직입니다. AES-256-GCM은 브라우저에서 끝나고 키는 # 뒤, 서버는 암호문만 잠시 둡니다. 만들기와 읽기 모두 가입이 없습니다. Network를 열고 위 단계로 방금 복사한 키를 검색하면 「로그에 나오면 안 되는 한 줄」이 회선에 탔는지 확인할 수 있습니다.
32 KB를 넘거나 여러 번 복호화할 백업이 필요하면 파일 상자로 기기에서 .lock / .enc를 만들고 옮기는 길은 직접 고릅니다. 열람 후 폐기가 푸는 것은 「한 번만 볼 짧은 비밀」이지 드라이브 대체품이 아닙니다. 민감한 계산을 왜 기본으로 서버에 넘기면 안 되는지는 이전 글 왜 암호화 계산을 브라우저 로컬에 두나가 평문 창을 더 엽니다. 이 글이 묻는 것은, 이 링크가 열릴 때 키가 아직 우물정 뒤에 있고 요청 줄로 달리지 않았는가입니다.