시작하며: 로직은 완벽한데 찝찝한 HomeLab 보안

지난 1편에서 루틴 알고리즘과 기본 노드 세팅을 완성했지만, 내 서버의 보안에 대한 찝찝함이 남았다. 구글 OAuth 연동을 위해서는 리다이렉트 URI로 공인된 https:// 도메인이 필수였고, 그 때문에 나는 어쩔 수 없이 리버스 프록시를 뚫어 내 서버를 외부에 열어두어야 했다.

‘단순히 나 혼자 쓰는 헬스 루틴 자동화 서버인데, 고작 인증 하나 때문에 잠재적인 외부 공격 위협을 감수해야 할까?’

이번 글은 인프라 보안을 위해 과감히 외부망을 차단하고, 구글 인증 방식을 ‘Service Account(서비스 계정)’ 로 우회하여 완벽한 로컬 폐쇄망을 구축한 나의 고민 일지다.

🧠 인프라 고민 일지

1. 방향 전환: “외부 접근의 문을 닫아버리자”

리버스 프록시를 걷어내고 집 안에서만 n8n.local 같은 내부 DNS(Pi-hole 활용) 주소로 쾌적하게 접근하기로 했다. 포트를 닫으면 보안도 챙길 수 있고, 프록시단에서 끊길 수 있는 여지가 있는 웹소켓 통신도 시원하게 뚫린다는 장점이 있었다.

2. 허들: “구글: 가짜 주소 안돼, 돌아가”

하지만 외부망을 닫자마자 예상대로 구글 API(OAuth) 인증이 막혀버렸다.

구글은 보안상 공인 도메인만 허용하므로, 내가 만든 http://n8n.local은 지도에 없는 가짜 주소 취급을 받았다. (Validation 실패)

3. 돌파구: “가짜 주소가 안 된다면, 비서를 직접 보내자”

고민 끝에 ‘Service Account’ 이라는 대안을 찾았다.

GCP Service Account는 사람이 아닌 애플리케이션, VM 인스턴스 등 머신이 다른 GCP 리소스(Cloud Storage, BigQuery 등)에 접근할 때 사용하는 인증된 ID이다. 비밀번호 대신 JSON 키 쌍을 사용해, 권한(IAM 역할)을 부여하여 최소 권한 원칙으로 리소스를 안전하게 관리하는 데 활용된다고 한다.

즉, 나의 본 계정을 통째로 연동하는 대신, 가상의 비서를 하나 채용해서 신분증(JSON 키)을 내 n8n에 쥐여주고 구글로 찾아가게 하는 방식이다. 이 방식은 철저히 내부망에서 구글 서버로 나가는(Outbound) 통신 이라, 내 서버의 외부 포트를 열 필요가 전혀 없는 최고의 해결책이었다.

4. 또 다른 허들: “n8n 구글 캘린더 노드: OAuth2만 됩니다”

비서를 채용했더니, n8n 2.x 버전의 Google Calendar 기본 노드는 사용자 편의를 위해 OAuth 방식만 하드코딩되어 있었다. 결국 1편에서 세팅했던 GET/POST 기본 노드를 버려야 했고, 만능 도구인 HTTP Request 노드를 활용해 구글 캘린더 API를 바닥부터 직접 찌르기로 했다.

🛠️ 폐쇄망 연동 세팅 가이드 (HTTP Request)

1. 전담 비서(Service Account) 채용 및 신분증 발급

가상의 로봇 비서를 구글에 공식 등록하고, n8n에 쥐여줄 신분증(JSON)을 발급받는 과정이다. GCP 인터페이스가 조금 복잡하지만 순서대로 따라 하면 된다.

  1. 서비스 계정 만들기: 구글 클라우드 콘솔에 접속해 1편에서 만든 프로젝트를 연다. 좌측 햄버거 메뉴에서 IAM 및 관리자 > 서비스 계정으로 이동한다. 상단의 [+ 서비스 계정 만들기] 를 누르고, 이름(예: n8n-calendar-bot)을 적고 완료한다. (권한 할당 등 복잡한 다음 단계들은 모두 무시하고 건너뛰어도 무방하다)

  2. 신분증(JSON) 발급: 생성된 서비스 계정 목록에서 방금 만든 비서 계정을 클릭하고, 상단의 [키] 탭으로 들어간다. [키 추가] > [새 키 만들기] 를 누르고, 키 유형을 JSON으로 선택하여 PC에 다운로드한다. 이것이 n8n 비서에게 쥐어줄 신분증이다.

  3. 이메일 주소 확보: 마지막으로 서비스 계정 세부정보에 적힌 긴 이메일 주소(~~~@~~~.iam.gserviceaccount.com)를 복사해 따로 적어둔다.

2. 내 캘린더를 비서에게 공유하기

비서는 내 본 계정과 완벽한 남남이므로, 내 다이어리를 입력/수정 할 수 있게 직접 초대해 주어야 한다.

  1. 내 구글 캘린더 웹페이지의 헬스 루틴 캘린더 [설정 및 공유] 로 들어간다.

  2. 1단계에서 복사해둔 비서의 긴 이메일 주소를 추가한다.

  3. 💡 (핵심) 권한을 반드시 [일정 변경(Make changes to events)] 으로 설정하고 저장한다.

3. n8n에 비서 신분증 등록

  1. n8n [Credentials] 탭에서 Google Service Account API 를 새로 추가한다.

  2. Region은 필수가 아닌 optional인 것 보니 그냥 Asia Pacific (Seoul) - asia-northeast3을 찾아서 설정했다.

  3. Service Account Email: 발급받은 비서의 이메일 주소

  4. Private Key: 아까 다운받은 JSON을 열어보면 privat_key가 존재한다. Begin 부터 end 사이에 존재하는 값을 복사해 넣어준다.

  5. 💡 (핵심) 하단의 Use with HTTP Request Node 옵션을 꼭 켠다.

  6. Scopeshttps://www.googleapis.com/auth/calendar 를 넣고 저장한다.

저장하면 알아서 유효한 키인지 검증을 진행한다.

4. HTTP Request [GET] 노드

기존 1편의 Google Calendar [GET] 노드를 지우고, HTTP Request 노드로 교체한다.

  • Method: GET

  • URL: https://www.googleapis.com/calendar/v3/calendars/내_캘린더_ID%40group.calendar.google.com/events (주소의 @%40으로 변환)

  • Authentication: Predefined Credential Type Google Service Account API 방금 만든 Credential 선택

  • Query Parameters:

    • Name : timeMin
      • Value : {{ new Date(new Date().setDate(new Date().getDate() - 14)).toISOString() }}
    • Name : timeMax
      • Value: {{ new Date().toISOString() }}
    • Name : singleEvents
      • Value: true

위 파라미터를 추가해 이전 달 14일 값 반복 일정을 낱개로 가져온다.

5. HTTP Request [POST] 노드 세팅 (새 일정 쓰기)

기존 1편의 Google Calendar [POST] 노드도 지우고, HTTP Request 노드로 교체한다. 이 노드는 구글의 까다로운 All Day 이벤트 규정에 맞춰 달력을 쓴다.

  • Method: POST

  • URL: GET 노드와 동일한 주소 입력

  • Authentication: Predefined Credential Type Google Service Account API 방금 만든 Credential 선택

  • Send Body: ON / Body Content Type: JSON / Specify Body: Using JSON

  • JSON

{{
  {
    "summary": $json.title,
    "start": {
      "date": $json.date
    },
    "end": {
      "date": $json.endDate
    }
  }
}}

마무리

이제 외부 포트 노출이라는 찝찝함 없이 완벽한 로컬 폐쇄망 안에서 달력에 일정을 쓸 수 있게 되었다! 하지만 무언가 아쉽다. 이 작업이 무사히 끝났음을 내 스마트폰으로 알려주는 텔레그램 연동을 해볼까 한다.