Site Icon THEREiS최신 글 게시판 · 008번 읽는 중

제 목 :

우리집 비서 만들기 프로젝트 #2 - 설계편

올린이
[야훔]
날짜
태그

지난 1편에서는 스마트 안경 앱을 만들려다가 집안 비서 Secretary를 만들게 된 이야기를 했다. Secretary는 시놀로지 NAS에서 Docker로 돌아가는 서비스 허브이고, 안경이든 폰이든 워치든 요청만 보내면 전등을 끄고, 메일을 보내고, 옵시디언에 메모를 남겨 준다.

문제는 이 서버를 밖에서도 쓰고 싶다는 데 있다. 지금처럼 출장 중일 때도, 퇴근길 지하철에서도 쓰려면 집 서버를 인터넷에 열어야 한다. 그런데 이 서버는 우리집 불을 끄고, 내 이름으로 메일을 보내고, 내 노트를 고칠 수 있다. 털리면 곤란한 정도가 아니다.

시큐어 인프라 글에서 “들어올 수 있는 문을 줄여라”라고 잔소리를 잔뜩 늘어놓았으니, 이번에는 내가 그걸 지킬 차례다.

처음 생각은 “10초짜리 토큰”이었다

처음 떠올린 방식은 이랬다. 클라이언트마다 Access Key와 Secret Key를 나눠 주고, 그걸로 10초짜리 임시 토큰을 발급받은 다음 그 토큰으로 업무 API를 호출하는 것이다. 토큰이 짧게 살수록 안전하다는 생각이었다.

그런데 따져 보니 구멍이 있었다.

  • Secret Key가 결국 클라이언트에 있다. 토큰을 발급받는 쪽과 Secret Key를 가진 쪽이 같은 기기다. 기기가 털리면 임시 토큰을 쓰든 말든 같이 털린다. 임시 토큰이 막아 주는 건 “통신 중에 토큰이 새서 재사용되는 경우” 정도다.
  • 호출할 때마다 왕복이 한 번 더 생긴다. 안경 → 폰 → NAS로 가는 경로에서 지연이 거의 두 배가 된다.
  • 10초는 너무 짧다. 기기 간 시계 차이, 모바일 네트워크 지연, 재시도까지 생각하면 토큰이 만료되는 일이 자주 생긴다.

그래서 방향을 바꿨다. 토큰을 발급받지 않고, 요청마다 Secret Key로 서명을 붙여 보내는 방식이다. AWS API를 호출할 때 쓰는 SigV4와 같은 원리다.

HMAC 방식 통신 개요

서명할 문자열 = METHOD \n PATH \n TIMESTAMP \n NONCE \n SHA256(body)
X-Access-Key: g2-glasses
X-Timestamp: 1761759600
X-Nonce: 7f3c9a...
X-Signature: HMAC-SHA256(secret, 서명할 문자열)

서버는 Access Key로 Secret을 찾아 서명을 다시 계산해 보고, 타임스탬프가 ±30~60초 안에 들어오는지, 같은 Nonce가 다시 오지 않았는지 확인한다. 이렇게 하면 Secret은 네트워크로 한 번도 나가지 않고, 가로챈 요청은 다시 쓸 수 없고, 왕복도 늘어나지 않는다. “짧은 유효시간”이라는 처음 의도도 타임스탬프 검증으로 그대로 살렸다.

보안은 층으로 쌓는다

HMAC 서명은 앱 단계의 잠금장치일 뿐이다. 전체 그림은 이렇게 다섯 층으로 잡았다.

  1. 네트워크: NAS 포트는 하나도 열지 않는다. 인증되지 않은 요청은 NAS에 닿지도 못하게 한다.
  2. 앱 인증: 기기마다 따로 키를 주고, 요청마다 HMAC 서명을 붙인다. 기기를 잃어버리면 그 키만 폐기한다.
  3. 작업 위험도: 조회는 서명만, 전등 제어는 서명과 권한 범위(scope)까지, 메일 발송이나 git push 같은 되돌릴 수 없는 작업은 다른 기기에서 한 번 더 승인받는다. 안경에서 메일을 보내려면 워치나 폰에서 탭해야 실제로 나간다. 삭제나 키 발급은 아예 밖에서는 못 하게 막는다.
  4. 피해 범위 줄이기: SES는 특정 발신 주소로 ses:SendEmail만 허용하는 IAM 키, Gitea는 보관소 저장소 하나에만 쓰기 권한이 있는 봇 계정을 쓴다. 컨테이너는 root가 아닌 사용자로 돌린다.
  5. 감시: 모든 요청은 감사 로그로 남기고, 인증 실패가 몰리거나 새 기기가 등록되면 카카오 ’나에게 보내기’로 알림을 받는다.

키 하나가 털려도 공격자가 할 수 있는 일이 “불 껐다 켜기” 수준에서 끝나게 만드는 게 목표다.

VPN이냐, Cloudflare냐

첫 번째 층, 네트워크를 어떻게 막을지는 두 가지 선택지가 있었다.

  • Tailscale 같은 VPN: 내 기기에서만 서버가 보인다. 인터넷에 아예 공개하지 않으니 가장 단단하다. 대신 폰에서 VPN이 항상 켜져 있어야 하고, 다른 VPN과는 같이 쓸 수 없다.
  • Cloudflare Tunnel + Cloudflare Access: NAS에서 Cloudflare 쪽으로 터널을 안에서 밖으로 뚫는다. 공유기 포트를 열 필요가 없다. 그리고 그 앞을 Access가 지키면서, 인증되지 않은 요청은 Cloudflare에서 바로 돌려보낸다.

G2도 워치도 결국 폰을 거쳐 서버로 가니 VPN도 충분히 가능했지만, 나는 클라이언트 쪽에 아무것도 깔지 않아도 되는 Cloudflare 쪽을 골랐다. 기기는 요청 헤더에 Access 서비스 토큰만 붙이면 된다.

비용도 따져 봤다. Cloudflare Zero Trust 무료 플랜에 Tunnel은 무제한, Access는 사용자 50명까지 들어 있다. 혼자 쓰는 집 서버라면 0원이다.

도메인 때문에 도메인을 하나 더 샀다

함정은 DNS였다. 내 도메인은 AWS Route53에서 관리하고 있는데, Cloudflare 무료 플랜은 도메인 전체의 DNS를 Cloudflare로 옮겨야만 쓸 수 있다. 서브도메인 하나만 넘기는 방식은 Business나 Enterprise 플랜 기능이다. 그 도메인에는 SES 메일 인증 레코드(DKIM, SPF, DMARC)가 잔뜩 걸려 있기도 하고 그 외에 온갖 서비스가 덕지덕지 붙어있는 도메인을 홈 서버 하나 때문에 메일 인프라를 건드리고 싶지 않았다.

그래서 Secretary 전용 도메인을 새로 샀다. ****.house로 샀다. 집에 있는 비서라서 house로 골랐다. 1년에 34.20달러 들었다. DNS서버 운영하는 회사도 운영비는 필요할 거라고 생각은 하지만 저렴한 개발에도 돈은 드는 구나 싶었다.

도메인 구입 인증

Cloudflare에서 직접 사니 네임서버를 바꿀 필요도 없이 DNS가 바로 Cloudflare에 올라갔다. 이번 프로젝트에서 유일하게 돈이 드는 부분이 이렇게 끝났다.

돈을 아껴야해요

Zero Trust 시작하기

Cloudflare의 Zero Trust라는 서비스(?) 같은게 있는데 이게 뭐랄까.. 서비스 인증이나 보안 관련한 서비스를 보다 쉽게 사용할 수 있도록 제공해주는 서비스다. 우리가 지금 작업하려는 터널 뚫기도 여길 통해서 작업을 해야하니 “Zero Trust 시작하기”를 시작(?)하자.

Zero Trust시작하기

어차피 개인용으로 사용하는거라서 요금제는 무료로 선택했다.

무료요금제 선택

이렇게 선택하면 활성화 화면이 나온다.

Zero Trust 활성화하기

Zero Trust 활성화하기

이 절차대로 진행하면 Zero Trust를 사용할 수 있게 된다.

터널 뚫기

Zero Trust를 시작했으면 이제 할일은 Cloudflare에서 터널을 뚫는 일인데.. 이 일은 일단 터널메뉴에 진입하자.

터널 메뉴 접근

Zero Trust 안에 있는 터널 메뉴에 진입한다.

터널 만들기 화면

터널 생성을 클릭하면서부터 시작이다.

터널 이름 적기

내 터널 이름은 “Home Secretary”로 정했다.

터널 만들기 화면

우리는 Docker로 환경을 만들거니까 Docker로 설정한다. Run tunnel with Docker에 있는 명령어를 복사하자. Docker 이미지를 만들때 명령어 안에 있는 토큰값이 필요하다. 그럼 이제 잠깐 NAS로 가자. Docker 이미지 만들러. Tunnel 페이지는 아직 닫지 말 것. Docker 이미지를 만들고 돌아올거다.

만약 닫았으면 처음으로 돌아가서 다시 진행해라.

Synology Container Manager 세팅하기

Container Manager 이미지 생성하기

앞서 말했듯 나는 Synology NAS의 Container Manager를 통해 Docker 이미지를 만들었다. Container Manager를 켜고 “프로젝트 > 생성”을 클릭한다.

Docker 이미지 생성 과정

이미지를 만들 docker-compose.yml의 내용은 아래와 같다.

services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
networks: [secretary_net]
whoami: # 연결 테스트용, 나중에 secretary로 교체
image: traefik/whoami
container_name: whoami
restart: unless-stopped
networks: [secretary_net]
networks:
secretary_net:
name: secretary_net

나중에 만들 서비스들도 같은 네트워크로 묶기 용이하려고 networks 설정도 넣었다. 이 내용에서 ${TUNNEL_TOKEN}은 아까 Tunnel 만들기 마지막 화면에서 Docker 이미지 명령어를 복사해서 붙여 넣으면 토큰값을 발견할 수 있다. 그 값으로 넣어준다.

Synology에 Docker 이미지 만들기

웹 포털은 사용하지 않는다. 왜냐하면 우린 포트를 열지도 않을거고 터널로만 통신을 할거니까. 밖에선 Cloudflare의 터널을 통하지 않으면 못들어오게 할거다. 그러니까 설정 하지 말 것!

Synology에 Docker 이미지 만들기

마지막 점검화면. “완료”를 클릭한다.

Synology에 Docker 이미지 만들기

정상적으로 이미지가 만들어졌다.

다시 터널 만들기 화면으로

정상적으로 Docker 이미지가 만들어졌다면 터널 만들고 있었던 창으로 돌아와서 아래와 같이 정상적으로 터널이 연결되었다는 메시지를 확인할 수 있다.

터널 연결 완료

“계속” 버튼을 누르자.

터널 만들기 완료

터널이 정상적으로 생성 된 것을 확인할 수 있다.

터널 만들기 정리

Zero Trust 대시보드에서 터널을 만들고(유형은 Cloudflared), 발급된 토큰으로 NAS에서 cloudflared 컨테이너를 띄운다. Secretary는 아직 없으니, 받은 요청의 헤더를 그대로 보여 주는 whoami 컨테이너를 테스트용으로 붙였다.

핵심은 whoami에 ports가 하나도 없다는 점이다. 이 컨테이너는 NAS 밖에서는 물론이고 집 LAN에서도 직접 접근할 수 없다. 오직 같은 Docker 네트워크에 있는 cloudflared를 통해서만 닿는다. 나중에 Secretary도 같은 방식으로 붙인다.

Access로 잠그기

서비스 토큰 만들기

다음은 문지기 설정이다. 기기들은 사람처럼 로그인할 수 없으니 서비스 토큰을 쓴다. 토큰을 만들면 Client ID와 Client Secret이 나오고, 요청 헤더에 CF-Access-Client-Id와 CF-Access-Client-Secret을 붙이면 통과한다. Secret은 만들 때 한 번만 보여 주니 바로 저장해 둬야 한다.

Access메뉴로 이동

일단 “서비스 자격증명” 페이지로 이동한다.

서비스 토큰

“서비스 토큰 생성” 버튼을 누른다.

서비스토큰 만들기

서비스 토큰을 만들 수 있는데 일단 테스트용으로 “test-curl”로 만들고 토큰 기간은 1년으로 잡았다.

서비스토큰 발급 완료

클라이언트 ID와 클라이언트 암호(Secret)가 발급되는데 어디 잘 적어두도록 하자. 클라이언트 ID는 이 창을 닫고 난 뒤에도 찾을 수 있지만 클라이언트 암호는 이 화면을 닫는 순간 영영 찾을 수 없기 때문이다.

애플리케이션 만들기

애플리케이션 메뉴 이동

애플리케이션으로 이동한다. 여기서 방금 발행한 토큰을 사용해 자격증명을 하는 애플리케이션을 만들거다.

애플리케이션 메뉴 이동

“새 애플리케이션 만들기” 버튼을 누른다.

애플리케이션 메뉴 이동

우리는 공개 DNS로 만들어야한다. 왜냐하면 외부 망에서도 접근이 가능해야하니까.

정책 만들기

이제 정책을 만들어야한다. 정책 규칙엔 “Service Token”으로 선택하고 값에는 방금 만들었던 test-curl을 선택한다. 정책을 만들 때 하나 주의할 점이 있다. 작업(Action)을 기본값인 허용이 아니라 위 이미지 처럼 서비스 인증으로 바꿔야 한다. 허용은 사람이 로그인할 때 쓰는 동작이라, 서비스 토큰만 넣고 허용으로 두면 로그인 페이지로 보내려고 해서 제대로 동작하지 않는다. 화면에도 친절하게 경고가 뜬다.

세팅을 완료하고 “정책저장”을 눌러 저장을 하고 나서 애플리케이션 생성을 하고 나면

애플리케이션 생성 완료

이렇게 애플리케이션이 정상적으로 생성된 것을 확인할 수 있다.

경로 연결하기

이제 터널에 새로 만든 애플리케이션을 등록해야한다. 터널 메뉴로 돌아와 아까 만들어뒀던 터널을 선택한다. 선택한 터널 아래에 경로를 추가할 수 있는 화면이 있다.

경로 추가

경로 추가버튼을 눌러 방금 만든 애플리케이션을 추가하자.

게시된 애플리케이션 추가

게시된 애플리케이션을 선택하자.

애플리케이션 정보 입력

이렇게 도메인과 “서비스 URL”을 입력해주고 “경로추가”를 클릭한다.

경로 추가 완료

추가가 완료된 메시지를 확인하고.

경로 추가 확인

이렇게 결과도 확인할 수 있다.

그리고 테스트

터미널
# 토큰 없이 → 403 Forbidden
curl -i https://api.xxxxxxx.house
# 토큰 포함 → 200
curl -s https://api.xxxxxxx.house \
-H "CF-Access-Client-Id: <ID>" \
-H "CF-Access-Client-Secret: <SECRET>"

토큰 없이 요청하기

터미널에서 토큰 없이 요청을 보내면 403 응답이 발생한다.

토큰 없이 요청하기 터미널에 토큰을 넣어서 보내면 200 응답이 발생하는 것을 확인해볼 수 있다.

추가해볼만한 것

JWT 검증하는 옵션을 추가해볼 수 있는데 서비스 자격증명 토큰은 Access 단계에서 검증하는 것이라면 JWT토큰 옵션은 우리집 Docker 이미지의 cloudflared 에서 한번 더 JWT 토큰으로 검증을 거치게 하는 옵션이다. 이걸 추가하는 방법은 아래와 같다.

어플리케이션 추가 설정

어플리케이션에 접근해서 추가설정을 선택한다. 화면 아래쪽으로 스크롤해서 내려가면 “AUD(애플리케이션 대상 그룹) 태그”이라는 항목을 확인할 수 있는데 여기 있는 토큰 값을 복사한다.

AUD 토큰 복사

그리고 터널로 돌아와 “경로” 탭에 들어가 “경로 편집” 메뉴를 선택한다.

경로편집 클릭

경로편집 클릭

애플리케이션 대상(AUD) 태그에 방금 복사해뒀던 코드를 넣고 저장한다.

AUD태그 입력

꼭 붙여넣고 엔터(Enter) 키를 입력하고 칩 형태로 만들고 나서 저장해야한다. 내가 이걸 몰라서 삽질을 했다. 이런 젠장.

그럼 경로가 성공적으로 저장되었다는 메시지를 확인할 수 있다. 그러면 터미널에 아래와 같은 명령으로 다시 테스트를 진행한다.

터미널
curl -s https://api.xxxxxxx.house \
-H "CF-Access-Client-Id: <새 ID>" \
-H "CF-Access-Client-Secret: <새 시크릿>" | grep -i jwt

아까의 명령에서 꽁무니에 grep -i jwt 만 붙은 형태다. 정상적으로 테스트가 되었다면 아래와 같이 JWT 값이 돌아온다. JWT 토큰 응답 테스트

지금까지의 구성

지금까지의 구성 정리하면 이렇다.

  • NAS 포트는 0개. 터널은 NAS에서 밖으로 뚫었고, 공유기는 아무것도 열지 않았다.
  • Cloudflare Access가 서비스 토큰이 없는 요청을 문 앞에서 돌려보낸다.
  • cloudflared가 Access JWT를 한 번 더 검증한다.
  • 앱 컨테이너는 ports 없이 Docker 네트워크 안에만 있다.
  • 비용은 도메인 하나. 나머지는 전부 무료다.

아휴 힘들어. 아이고 삭신이야

이번에 배운 것

  • 테스트는 “되는지”보다 “막히는지”부터. 토큰을 붙인 요청이 성공했다고 만족했다면, 문이 활짝 열려 있다는 걸 한참 뒤에 알았을 것이다.
  • 방어는 겹쳐 둔다. Access 하나만 믿지말고 JWT 검증 한 겹이 더 있으면, 한 군데 실수해도 버틸 수 있다.

다음 편에서는 드디어 whoami를 치우고, 그 자리에 Secretary 서버를 올린다. HMAC 인증과 기기 키, 그리고 SES 메일과 Gitea·옵시디언 연동부터 시작할 생각이다.

그럼 안녕!
아디오스

[ 꼬리말 ]승인 후 게시됩니다

꼬리말을 불러오는 중...