호스트 Nginx 전환
이 디렉터리에는 호스트 수준 Nginx 신뢰 경계의 설정 원본이 있다. 이 파일은
Kubernetes 리소스가 아니며 Argo CD에서 조정하지 않는다.
2026-07-23 현재 활성 설정은 Gitea와 Keycloak을 함께 proxy하는
learn-services-keycloak.conf이며 SHA-256은
5c5cd74b4992f537fd27c50cf2209573a80a9904e0b19b58c3154717be6ff4a5다.
전환 전 설정은
/etc/nginx/sites-available/learn-services.before-keycloak-20260723160519에
백업했다. Nginx proxy 상태는 두 번의 probe 뒤 안정화됐고 Gitea health, Keycloak
discovery issuer, HTTPS cookie·redirect와 미등록 hostname 거부 검사를 통과했다.
후속 Gitea OIDC·브랜딩 rollout도 manifest SHA-256 d25a757...a157로 완료했으며
OAuth source·정책·authorization-code redirect·브랜딩 자동 검증을 통과했다. 실제
realm 사용자의 브라우저 login/callback/logout, 비상 관리자 실제 로그인과 Pod
재시작 뒤 설정 지속성은 별도 수용 시험으로 남아 있다. 인증이 필요한 Git
clone/push/reclone 시험도 아직 남아 있다.
계약
- 공개 포트
80과443은 Host Nginx에서 종료한다. - TLS는 Host Nginx에서만 종료한다.
git.learn.hyeonworks.com은 HTTP를 통해 Traefik의 loopback NodePort인127.0.0.1:30080으로 proxy한다.- Host Nginx는 들어오는
Host,X-Forwarded-*,X-Real-IP값을 교체한다. 특히 신뢰할 수 없는 클라이언트가 제공한X-Forwarded-Forchain을 이어 붙이지 않는다. id.learn.hyeonworks.com도 같은 loopback NodePort의 Keycloak Host route로 proxy한다.- Traefik HTTPS NodePort인
30443은 이 경로에서 사용하지 않는다.
2026-07-23 현재 kube-proxy의 nodePort-addresses=127.0.0.0/8 설정을 적용했다.
서버의 loopback 127.0.0.1:30080은 Traefik에 도달하지만, 서버와 같은 LAN의
노트북에서 192.168.0.107:30080 및 30443 연결은 모두 거부되는 것을 확인했다.
따라서 Host Nginx를 우회하는 LAN NodePort 경로는 현재 닫혀 있다.
현재 설정 원본은
learn-services-keycloak.conf다. 전환 전
learn-services.conf는 Keycloak 정적 hold가 포함된
rollback 기준으로 보존한다. 등록되지 않은 TLS hostname이 첫 번째 virtual host인
Gitea로 흘러가지 않도록 별도의 default_server가 ssl_reject_handshake on으로
handshake를 거부한다.
활성 Keycloak 설정은 같은 Gitea proxy를 보존하면서 id.learn.hyeonworks.com만
http://127.0.0.1:30080의 Traefik Host route로 바꾼다. 이는 Keycloak을 모든
서비스 앞의 인증 middleware로 두는 구성이 아니다. Gitea가 Keycloak을 독립 OIDC
Provider로 사용하는 데 필요한 네트워크 reverse proxy다.
2026-07-23 확인한 인증서는 CN이 git.learn.hyeonworks.com이고 SAN에
git.learn.hyeonworks.com, id.learn.hyeonworks.com을 모두 포함한다. 발급자는
Let's Encrypt YE2, 유효기간은 2026-07-18부터 2026-10-16까지다. Snap Certbot
5.7.0과 snap.certbot.renew.timer 활성 상태도 확인했다. 이 값은 점검 시점의
스냅샷이므로 설정 또는 인증서가 변경될 때 다시 검사한다.
사전 조건
다음 검사를 모두 통과하기 전에는 Nginx를 전환하지 않는다.
-
Gitea 워크로드, Service 및 Ingress가 Ready 상태다.
-
Ingress는
git.learn.hyeonworks.com을 사용하며 Kubernetes TLS block이 없다. -
다음 Traefik 직접 probe가 Gitea health 응답을 반환한다.
curl --fail-with-body \ --header 'Host: git.learn.hyeonworks.com' \ http://127.0.0.1:30080/api/healthz -
별도 LAN 호스트에서
192.168.0.107:30080과192.168.0.107:30443의 TCP 연결이 모두 거부되거나 timeout되는지 확인한다.nc -vz -w 3 192.168.0.107 30080 nc -vz -w 3 192.168.0.107 30443TCP 연결이나 HTTP 응답이 하나라도 성공하면 이 차단 조건을 통과하지 못한 것이다. Traefik
404는 포트가 차단된 것이 아니라 정상적으로 도달했다는 증거다. 포트 하나라도 도달할 수 있으면 전환을 중지하고../traefik/README.md의 제한 지침을 따른다. -
활성 인증서가 이 파일에 남아 있는 두 hostname을 모두 포함한다.
sudo certbot certificates -
sudo nginx -t와 Nginx 서비스 상태 검사를 통과한다.
최초 Gitea 전환 절차(과거 기록)
다음 절차는 Keycloak 전환 전 Gitea-only 설정을 처음 적용했을 때의 기록이다. 현재 활성 Keycloak 설정에 이 스크립트를 재실행하지 않는다. 당시에는 Argo CD가 아니라 의도적인 호스트 작업으로 저장소 루트에서 다음 스크립트를 실행했다.
cd /home/donghyeon/workspace/platform
sudo bash scripts/bootstrap/apply-host-nginx-gitea.sh --execute
스크립트가 출력한 활성/후보 SHA-256과 백업 경로를 확인한 뒤 prompt에 정확히
APPLY를 입력한다. 스크립트는 다음 작업을 한 단위로 수행한다.
- 기존 활성 파일을 timestamp가 붙은 root 소유 파일로 백업하고 해시를 검증한다.
- 후보를 root:root, mode 0644로 설치한 뒤
nginx -t, reload, active 상태를 검사한다. - reload 직후 기존 placeholder fingerprint만 bounded retry하고, 정상 Gitea health
JSON을 두 번 연속 확인해야 다음 검사로 진행한다. 다른
200비JSON 응답은 오라우팅으로 즉시 실패한다. - loopback TLS 경로의 Gitea health, HTTP→HTTPS 301, 로그인 쿠키의
Secure, Keycloak hold 응답, 미등록 TLS hostname 거부를 검사한다. - 활성 파일 변경 뒤 포착 가능한 오류가 발생하거나 INT/TERM signal로 중단되면
정확한 백업을 자동 복원하고
nginx -t와 reload를 다시 수행한다. - SIGKILL 또는 전원 장애처럼 trap이 실행될 수 없는 중단은 자동 복구 대상이 아니며, 재접속 후 출력된 백업 경로로 수동 복구한다.
서버에서는 NAT hairpin이 지원되지 않아 공인 주소를 향한 요청이 timeout될 수 있다.
따라서 서버 로컬 검증은 스크립트처럼 --resolve ...:127.0.0.1을 사용한다. 실제
공개 경로는 외부망(예: 모바일 핫스팟)에 연결된 별도 클라이언트 또는 외부 probe에서
--resolve 없이 검증한다. 아래 명령과 정적 hold 기대값은 최초 Gitea-only 전환
당시의 검사이며 현재 Keycloak 공개 경로의 수용 기준이 아니다.
curl --fail-with-body https://git.learn.hyeonworks.com/api/healthz
curl --fail-with-body https://id.learn.hyeonworks.com/
두 번째 응답은 계속 Keycloak domain reached Nginx successfully여야 한다. 실패 시
스크립트가 출력한 정확한 백업 경로를 사용해 다음 순서로 수동 복구한다.
sudo install -o root -g root -m 0644 BACKUP_PATH /etc/nginx/sites-available/learn-services
sudo nginx -t
sudo systemctl reload nginx
전환 기록에는 활성/후보/백업 SHA-256, 실제 백업 경로, nginx -t, reload, 로컬
health와 redirect, 값은 숨긴 Cookie 속성, 외부 health, Git clone/push/reclone 결과를
남긴다.
2026-07-23 1차 전환의 reload readiness race와 자동 롤백, 2차 전환 성공, 공개 경로 검증 결과는 중앙 실행 기록에 보존한다. 사용자 인증이 필요한 Git clone/push/reclone은 아직 남아 있으므로 그 결과도 같은 문서에 추가한다.
Keycloak proxy 전환
완료 상태와 선행 조건
Keycloak 후보는 2026-07-23 활성화했다. 다음 항목은 전환 전에 모두 통과한 선행 조건이다.
-
keycloaknamespace의 공식 Keycloak Operator와 Server26.7.0이 Ready다. -
hyeonworksrealm의 내부 discovery가 JSON으로 응답하고 issuer가 정확히https://id.learn.hyeonworks.com/realms/hyeonworks다.curl --disable --noproxy '*' \ --fail-with-body --silent --show-error \ --header 'Host: id.learn.hyeonworks.com' \ --header 'X-Forwarded-Host: id.learn.hyeonworks.com' \ --header 'X-Forwarded-Proto: https' \ --header 'X-Forwarded-Port: 443' \ http://127.0.0.1:30080/realms/hyeonworks/.well-known/openid-configuration \ | jq --exit-status \ '.issuer == "https://id.learn.hyeonworks.com/realms/hyeonworks"' -
Traefik
webentrypoint가 Host Nginx 경로에서 실제로 관측한 한 주소10.42.0.1/32만 신뢰한다.forwardedHeaders.insecure와websecuretrust는 없어야 한다.kubectl -n kube-system get helmchartconfig traefik \ -o jsonpath='{.spec.valuesContent}' kubectl -n kube-system get deployment traefik -o json \ | jq --raw-output \ '.spec.template.spec.containers[] | select(.name == "traefik") | .args[]' \ | rg 'forwardedHeaders|accesslog'기대하는 trust runtime 인자는 다음 한 줄이다.
--entryPoints.web.forwardedHeaders.trustedIPs=10.42.0.1/32 -
Keycloak Ingress backend는
keycloak-service:8080뿐이며 관리 포트9000은 Ingress, NodePort, LoadBalancer와 Host Nginx 후보에 연결되지 않는다.kubectl -n keycloak get ingress keycloak-http -o wide kubectl -n keycloak get service keycloak-service -o wide rg -n '9000|keycloak-service|proxy_pass' \ infrastructure/networking/host-nginx/learn-services-keycloak.confkeycloak-service자체가 내부ClusterIP에서9000을 제공하는 것은 Operator의 관리 interface 계약이다. 실패 조건은 이 포트를 외부 경로에 연결한 Ingress, NodePort, LoadBalancer 또는 Nginxproxy_pass가 존재하는 경우다. -
Gitea health, NodePort loopback 경계, 두 hostname을 포함하는 인증서, Nginx active 상태와
nginx -t가 계속 통과한다.
2026-07-23 내부 hyeonworks discovery의 issuer·endpoint, Traefik의
10.42.0.1/32 최소 trust와 관리 포트 9000 미노출을 확인한 뒤 public
cutover를 완료했다. 활성 Nginx SHA-256은
5c5cd74b4992f537fd27c50cf2209573a80a9904e0b19b58c3154717be6ff4a5다.
실제 실행 명령과 결과
저장소 루트에서 다음 root 작업을 실행했다.
cd /home/donghyeon/workspace/platform
sudo bash scripts/bootstrap/apply-host-nginx-keycloak.sh --execute
스크립트가 보여 준 후보·활성 파일과 SHA-256, backup 경로를 확인한 뒤 prompt에 정확히 다음을 입력했다.
APPLY
스크립트는 후보 SHA-256
5c5cd74b4992f537fd27c50cf2209573a80a9904e0b19b58c3154717be6ff4a5와
전환 전 활성 SHA-256
de7ebd4f69cd7d2204ee633e074bf6a4370a6f5e3f3fac9067b099d5d75269b5를
고정 gate로 확인한다. 예상하지 않은 활성 설정이면 덮어쓰지 않고 중단한다.
실제 backup은
/etc/nginx/sites-available/learn-services.before-keycloak-20260723160519이며,
두 번의 probe 뒤 proxy 상태가 안정화됐다. Gitea health, Keycloak discovery
issuer, HTTPS cookie·redirect와 미등록 hostname 거부 자동 검사를 모두 통과했다.
자동 backup, 검증과 rollback
전환 스크립트는 다음을 한 단위로 수행한다.
- 변경 전에 직접 Traefik 경로의 Gitea health와
hyeonworksdiscovery issuer를 검증한다. - 활성
/etc/nginx/sites-available/learn-services를learn-services.before-keycloak-<timestamp>로 백업하고 digest를 보존한다. - 후보를
root:root 0644로 설치하고nginx -t가 성공한 경우에만 reload한다. - reload 뒤 Gitea health와 Keycloak discovery가 함께 수렴할 때까지 bounded retry한다.
- Git과 ID hostname의 HTTP→HTTPS
301, Gitea 로그인 Cookie의Secure, Keycloak discovery issuer·endpoint, 미등록 TLS hostname 거부와 node IP의30080/30443차단을 검사한다. - 활성 파일을 바꾼 뒤 오류 또는 INT/TERM이 발생하면 정확한 backup을 복원하고
nginx -t, reload, Gitea health와 기존 Keycloak static hold가 돌아왔는지 다시 확인한다.
자동 rollback이 성공하면 전환 전 Gitea proxy와 Keycloak static hold 상태로 복귀한다. SIGKILL이나 전원 장애처럼 trap이 실행되지 않은 경우에는 스크립트가 출력한 정확한 backup 경로를 사용한다.
sudo install -o root -g root -m 0644 \
BACKUP_PATH \
/etc/nginx/sites-available/learn-services
sudo nginx -t
sudo systemctl reload nginx
전환 성공 후 Gitea OIDC source와 브랜딩을 포함한 manifest도 적용했고 자동 수용 검사를 통과했다. 실제 realm 사용자의 브라우저 login/callback/logout, 비상 관리자 실제 로그인과 Pod 재시작 뒤 설정 지속성은 별도 단계다. 완료 상태와 후속 수용 기준은 중앙 Keycloak·Gitea OIDC 실행 기록에 보존한다.
Observability 단계 전환
Observability 전환은 기존 site 전체를 임의로 재생성하지 않고 다음 네 mode만 사용한다. 인자 없는 호출은 공개 Grafana DNS 부재와 source/active SHA-256만 읽는 dry-run이다.
cd /home/donghyeon/workspace/platform
bash scripts/bootstrap/apply-host-nginx-observability.sh
bash scripts/bootstrap/apply-host-nginx-observability.sh --execute --metrics-guard-only
bash scripts/bootstrap/apply-host-nginx-observability.sh \
--execute --certificate-only --certbot-email you@example.com
bash scripts/bootstrap/apply-host-nginx-observability.sh --execute --grafana-deny-guard-only
bash scripts/bootstrap/apply-host-nginx-observability.sh \
--execute --verified-output-dir "$METRIC_ROOT"
모든 config mutation은 PLATFORM_OBSERVABILITY_ROLLBACK_ID가 가리키는 root-only
rollback root 아래 host-nginx/stages.tsv와 host-nginx/payloads/에 직전 active
bytes와 양쪽 SHA-256을 먼저 기록한다. 실패 시 timestamp backup이 아니라 이 payload를
hash 검증해 복원한다. full mode는 active deny-guard SHA 외에도 같은 rollback ID의
blackbox-source-proof.env, access-rules-alerts/acceptance.env와
$METRIC_ROOT/{target-initial,post-substrate}/inventory.sha256 결합이 정확해야만 prompt를
표시한다. 인증서 private key와 Cloudflare token은 ledger에 기록하지 않는다.
범위 경계
Debian/Ubuntu의 sites-enabled/* 파일은 Nginx의 http context에서 include되므로
map directive를 사용할 수 있다. 이 include 구조가 변경되면 설치 전에 다시
검증한다. 두 hostname 중 어느 쪽에도 활성 server block을 하나 더 만들지 말고 기존
learn-services 파일을 하나의 단위로 교체한다.
NodePort 주소 제한은 k3s의
/etc/rancher/k3s/config.yaml.d/30-nodeport-loopback.yaml에서 관리한다. 변경
후에는 Host Nginx의 127.0.0.1:30080 접근과 LAN의 node-IP 접근 거부를 항상 함께
재검증한다. Router firewall/NAT는 이 저장소가 자동으로 변경하지 않는다.