n8n 웹훅 인증: Header Auth와 HMAC 선택 기준
비공개 IP 뒤의 n8n 웹훅도 인증이 필요한 이유와 Header Auth, JWT, HMAC·타임스탬프·nonce를 상황에 맞게 고르는 기준을 설명한다.
이 글의 목차비공개 IP와 인증은 서로 다른 질문이었다10
자동화 서버가 외부에 공개 IP를 갖고 있지 않으니 웹훅도 외부에서 함부로 호출할 수 없을 것이라는 이야기가 나왔다.
구성도를 보면 그럴듯했지만, 실제 요청이 지나가는 길은 서버의 IP 하나로 결정되지 않았다.
웹훅은 다른 시스템이 특정 주소로 HTTP 요청을 보내 작업을 시작시키는 입구다.
n8n에서는 Webhook 노드가 요청을 받으면 연결된 자동화 워크플로를 실행한다.
과거 내부 점검 당시에는 추측 대신 알려진 웹훅 주소에 인증 정보 없이 시험 요청을 보냈다.
그 재현에서는 응답이 HTTP 200이었고 워크플로가 시작됐다.
그 워크플로가 고객 문의를 저장하는 단순 자동화라면 원하지 않는 데이터가 쌓일 수 있다.
AI 요약, 문자 발송이나 유료 외부 API를 호출한다면 같은 요청의 반복이 비용과 실제 사용자 알림으로 이어질 수도 있다.
이 과거 결과가 현재 엔드포인트도 같은 상태라는 뜻은 아니다.
실제 엔드포인트와 현재 운영 구성은 공개하지 않으며, 당시 재현에서 일반화한 방어 기준과 검증 계약만 다룬다.
비공개 IP와 인증은 서로 다른 질문이었다
서버에 공개 IP가 없어도 리버스 프록시, 로드 밸런서나 터널이 외부 요청을 받아 내부 서버로 전달할 수 있다.
리버스 프록시는 건물 안내 데스크처럼 외부 요청을 먼저 받고 안쪽 서비스로 넘기는 서버다.
네트워크 경계는 “이 요청이 서버까지 도착할 수 있는가”를 제한한다.
애플리케이션 인증은 “도착한 요청을 보낸 주체를 믿을 수 있는가”를 확인한다.
보안그룹이나 방화벽으로 접근 범위를 좁히는 일도 필요하지만 웹훅 인증을 대신하지는 않는다.
네트워크 규칙을 안전하게 바꾸는 순서는 보안그룹 규칙을 추가한 뒤 제거한 기록에 정리했다.
URL을 아는 것을 신원으로 쓰면 생기는 문제
길고 추측하기 어려운 웹훅 주소는 우연한 호출을 줄이는 데 도움을 준다.
그러나 주소는 로그, 브라우저 기록, 분석 도구, 채팅과 오류 화면을 통해 복사될 수 있다.
주소가 유출됐을 때 요청자와 공격자를 구분할 다른 정보가 없다면 URL 자체가 비밀번호가 된다.
URL을 바꾸는 회전은 유출 뒤 대응 수단이 될 수 있지만 매 요청의 신원을 증명하지는 못한다.
Presigned URL도 주소가 가진 임시 권한이라는 점에서 비슷한 주의가 필요하다.
관련 경계는 S3 Presigned URL이 막지 못하는 것에서 다뤘다.
이번 점검에서는 네 가지 위험을 따로 적었다.
- 인증 없는 호출이 비용이 큰 AI 작업이나 외부 API 호출을 반복해서 실행할 수 있다.
- 이전의 정상 요청을 그대로 다시 보내는 재전송 공격을 구분하기 어렵다.
- 프록시와 n8n 사이의 경계를 잘못 이해하면 내부 서비스라고 믿은 주소가 외부 경로에 연결될 수 있다.
- 누가 어떤 요청을 보냈는지 남지 않으면 사고 뒤 범위를 확인하기 어렵다.
n8n이 제공하는 인증부터 켤 수 있다
n8n Webhook 자격 증명은 Basic Auth, Header Auth, JWT Auth와 인증 없음 옵션을 제공한다.
Basic Auth는 사용자 이름과 비밀번호를, Header Auth는 정해 둔 헤더 이름과 값을, JWT Auth는 서명된 JSON Web Token을 확인한다.
호출 주체가 한두 개의 내부 서비스라면 충분히 긴 임의 토큰을 Header Auth로 전달하는 방식부터 시작할 수 있다.
토큰은 워크플로 JSON이나 소스 코드에 직접 쓰지 않고 자격 증명 저장소에서 관리하며, 유출 시 교체할 수 있어야 한다.
사용자별 권한과 만료가 필요하면 JWT가 더 맞을 수 있다.
다만 어떤 방식을 선택하든 HTTPS로 전송 구간을 암호화하고 실패 요청을 기록하며 호출 빈도 제한을 함께 둬야 한다.
인증 방식을 고를 때는 호출자 수와 검증할 속성을 기준으로 삼을 수 있다.
| 상황 | 시작하기 좋은 방식 | 추가로 확인할 것 |
|---|---|---|
| 한두 개 내부 서비스가 정해진 자동화를 호출 | 충분히 긴 임의값을 쓴 Header Auth | HTTPS, 토큰 교체, 호출 빈도 |
| 여러 사용자·서비스마다 만료와 권한이 다름 | JWT Auth | 발급자, 대상 audience, 만료와 서명키 |
| 결제·주문처럼 본문이 바뀌지 않았음을 확인해야 함 | 앞단 검증 계층의 HMAC | 원본 본문, 타임스탬프와 nonce |
| 브라우저의 일반 사용자가 직접 호출 | 애플리케이션 로그인 세션을 먼저 검증 | CSRF, 사용자별 권한과 사용량 제한 |
HMAC은 n8n Webhook 노드의 기본 인증 옵션을 이름만 바꿔 쓰는 기능이 아니다.
원본 바이트와 재전송 방지 상태를 다뤄야 하므로 리버스 프록시나 작은 검증 서비스에서 확인한 뒤 n8n으로 전달하는 구성이 더 명확하다.
본문이 바뀌지 않았는지 확인하려면 HMAC을 더한다
이번 작업의 범위는 위험 확인과 방어 설계 확정이었다.
운영에 바로 옮길 수 있도록 권고안을 다음 검증 계약으로 구체화했다.
HMAC은 발신자와 수신자만 아는 비밀키로 요청 본문의 서명을 만드는 방식이다.
수신자는 같은 계산을 해 서명이 일치하는지 확인하므로 전송 중 본문이 바뀌지 않았고 비밀키를 가진 발신자가 보냈는지 검사할 수 있다.
여기서 암호화와 서명은 다르다.
HTTPS는 전송 중 내용을 제삼자가 읽거나 바꾸기 어렵게 하고, HMAC은 양쪽이 공유한 키와 받은 바이트로 요청의 무결성과 발신자 보유 비밀을 확인한다.
HMAC을 추가해도 본문 자체가 암호문으로 저장되는 것은 아니다.
서명에는 파싱한 JSON이 아니라 수신한 원본 바이트를 사용해야 한다.
JSON은 공백이나 키 순서가 달라도 같은 값으로 해석될 수 있지만 바이트가 달라지면 HMAC 결과도 달라지기 때문이다.
다음 예시는 n8n 앞의 Node.js 검증 계층에서 사용할 수 있는 단순화한 코드다.
import {
createHmac,
timingSafeEqual,
} from "node:crypto";
function verifyWebhook(
rawBody: Buffer,
timestamp: string,
nonce: string,
receivedHex: string,
secret: Buffer,
) {
const expected = createHmac("sha256", secret)
.update(timestamp)
.update(".")
.update(nonce)
.update(".")
.update(rawBody)
.digest();
const received = Buffer.from(receivedHex, "hex");
if (
received.length !== expected.length ||
!timingSafeEqual(received, expected)
) {
throw new Error("invalid webhook signature");
}
}createHmac("sha256", secret)은 SHA-256 해시 함수와 비밀키로 서명 계산기를 만든다.
update()에는 양쪽이 약속한 순서대로 타임스탬프, 구분자, nonce, 구분자와 원본 본문을 넣는다.
구분자는 각 값의 경계가 모호해지지 않도록 발신자와 수신자가 같은 문자로 고정한다.
digest()는 비교할 바이트를 반환한다.
timingSafeEqual()은 값이 어디에서 처음 달랐는지 실행시간으로 추측하기 어렵게 같은 길이의 바이트를 비교한다.
두 값의 길이가 다르면 예외가 발생하므로 길이를 먼저 확인한다.
Node.js 문서도 이 함수 하나만 쓴다고 주변 코드 전체의 시간차 공격이 자동으로 막히는 것은 아니라고 경고한다.
비밀키는 일반 문자열 상수보다 KeyObject나 안전한 비밀 저장소에서 가져온 바이트로 다루는 편이 낫다.
로그에는 비밀키, 서명 전체와 원본 개인정보를 남기지 않는다.
이 함수는 서명 일치만 검사하는 단순화한 예시다.
타임스탬프가 허용 시간 안에 있는지 검사하는 단계와 nonce를 공통 저장소에 원자적으로 기록하는 단계는 HMAC 검증과 별도로 구현해야 한다.
서명만 확인하면 정상 요청도 다시 실행할 수 있다
공격자가 정상 요청과 서명을 통째로 복사했다면 같은 요청을 다시 보낼 수 있다.
HMAC 계산이 맞기 때문에 서명 검증만으로는 새 요청인지 구분하지 못한다.
그래서 발신 시각을 나타내는 타임스탬프와 한 번만 쓰는 임의값인 nonce를 서명 대상에 포함한다.
수신자는 현재 시각과 허용 오차를 비교하고, 이미 처리한 nonce라면 요청을 거부한다.
nonce 저장 기간은 허용한 재전송 시간보다 길어야 한다.
여러 수신 인스턴스가 있다면 각 인스턴스의 메모리가 아니라 공통 저장소에서 원자적으로 “처음 사용” 여부를 기록해야 한다.
권고한 요청 처리 순서는 다음과 같다.
- 프록시가 본문 크기와 요청 빈도를 제한한다.
- 검증 계층이 필수 헤더와 타임스탬프 허용 범위를 확인한다.
- 원본 본문으로 HMAC을 계산해 같은 길이의 서명과 비교한다.
- nonce를 공통 저장소에 처음 한 번만 기록한다.
- 검증을 통과한 요청만 n8n 워크플로에 전달한다.
- 성공과 거부를 비밀값 없이 감사 로그에 남긴다.
WAF는 Web Application Firewall의 약자로 알려진 공격 패턴과 비정상 트래픽을 앞단에서 거른다.
속도 제한과 WAF는 대량 호출의 피해를 줄이지만 올바른 발신자 신원을 증명하지는 않으므로 HMAC이나 토큰 인증과 함께 사용한다.
배포 전에는 성공보다 실패 요청을 먼저 검증한다
방어 기능은 정상 요청이 통과하는지만 보면 절반만 확인한 셈이다.
다음 여섯 요청을 자동 테스트로 고정하면 인증, 무결성, 재전송 방지와 남용 제한의 경계를 각각 확인할 수 있다.
| 시험 요청 | 기대 결과 |
|---|---|
| 인증 헤더나 서명이 없음 | 401 또는 403으로 거부하고 워크플로를 실행하지 않음 |
| 서명 뒤 본문 한 글자를 바꿈 | HMAC 불일치로 거부 |
| 허용 시간을 지난 타임스탬프 | 오래된 요청으로 거부 |
| 같은 nonce와 서명을 두 번 보냄 | 첫 요청만 통과하고 두 번째 요청은 거부 |
| 유효한 서명과 새 nonce | 워크플로를 정확히 한 번 실행 |
| 짧은 시간에 제한 이상 호출 | 프록시나 WAF에서 제한하고 감사 로그에 사유 기록 |
401은 인증 정보가 없거나 유효하지 않다는 HTTP 상태이고, 403은 요청자를 알지만 해당 작업을 허용하지 않는다는 상태다.
둘 중 어떤 값을 사용할지는 시스템의 인증 경계에 맞춰 하나로 정하고, 외부 응답에 내부 검증 실패의 세부 이유를 과하게 노출하지 않는다.
검증 로그에는 요청 식별자, 발신자 식별자, 검증 결과와 처리시간을 남긴다.
비밀키, 전체 서명, 웹훅의 비밀 경로와 개인정보 본문은 남기지 않는다.
점검으로 확정한 방어 기준
과거 점검에서 확인한 사실은 인증 정보가 없는 시험 요청이 HTTP 200을 받고 워크플로를 시작했다는 점이다.
네트워크 비공개 여부와 웹훅의 애플리케이션 인증이 별개라는 결론도 실제 호출 경로를 통해 확인했다.
이 재현을 바탕으로 기본 인증, 본문 HMAC 서명, 타임스탬프와 nonce, 속도 제한, WAF와 감사 로그를 서로 다른 방어 계층으로 정리했다.
적용의 최소선은 HTTPS, Header Auth와 속도 제한이고, 본문 무결성과 재전송 방지가 필요하면 HMAC, 타임스탬프와 nonce 검증까지 포함한다.
서명 없음, 본문 변조, 만료된 타임스탬프와 nonce 재사용을 모두 거부하고 정상 요청을 한 번만 실행하는 자동 테스트를 통과해야 이 작업을 완료로 본다.
웹훅을 공개하기 전 확인할 목록
- 외부에서 웹훅까지 이어지는 프록시, 로드 밸런서와 터널 경로를 그린다.
- n8n Webhook 노드의 인증 옵션이
None인지 먼저 확인한다. - URL을 비밀로 취급하더라도 별도의 토큰, JWT 또는 HMAC 검증을 둔다.
- HMAC은 원본 본문과 타임스탬프 및 nonce를 같은 형식으로 서명한다.
- 허용 시간을 넘거나 이미 사용한 nonce를 거부한다.
- 요청 본문 크기, 사용자 또는 발신자별 호출 수와 동시 실행 수를 제한한다.
- 워크플로가 실행할 수 있는 외부 API와 자격 증명의 권한을 최소화한다.
- 실패와 성공을 추적하되 URL의 비밀 부분과 개인정보는 로그에서 가린다.
- 비밀키 교체 절차와 이전 키를 허용할 짧은 전환 시간을 정한다.
서버가 안쪽에 있다는 사실만으로 요청자를 믿지 않는다
이번 점검은 새로운 공격을 발견한 일이 아니라 믿고 있던 경계를 실제 요청으로 확인한 일이었다.
공개 IP가 없다는 설명만으로는 프록시 뒤의 웹훅이 인증됐다고 말할 수 없었다.
웹훅 주소는 요청이 도착할 위치이고 인증 정보는 누가 보냈는지 판단할 근거다.
두 역할을 분리하면 네트워크 구성이 바뀌어도 실행 권한의 기준이 URL 하나에 매달리지 않는다.
단순한 내부 서비스 호출이라면 HTTPS와 n8n Header Auth 및 속도 제한을 최소 기준으로 두고, 본문 무결성과 재전송 방지까지 필요하면 n8n 앞의 검증 계층에서 HMAC, 타임스탬프와 nonce를 확인한 뒤 검증된 요청만 전달하는 구조로 문제를 닫을 수 있다.