Add platform infrastructure configuration
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# AIStor 네트워크 정책
|
||||
|
||||
이 디렉터리는 AIStor 제어 영역(`aistor`)과 데이터 영역(`object-storage`)의
|
||||
NetworkPolicy를 한 곳에서 관리한다.
|
||||
|
||||
- 두 namespace의 모든 Pod는 기본적으로 ingress와 egress가 차단된다.
|
||||
- DNS와 k3s Kubernetes API 접근만 명시적으로 허용한다.
|
||||
- API server에서 admission webhook `8443/TCP`로 들어오는 트래픽을 허용한다.
|
||||
- ObjectStore Pod와 Operator 사이에는 관리 포트만 허용한다.
|
||||
- S3 `9000/TCP`와 HTTP Console `9090/TCP`는 `object-storage` namespace 내부
|
||||
클라이언트만 사용할 수 있다.
|
||||
- Operator와 sidecar 관리에는 `4221/TCP`, `4444/TCP`만 허용한다.
|
||||
- 외부 인터넷, Traefik, NodePort, LoadBalancer, Host Nginx 경로는 열지 않는다.
|
||||
|
||||
`learning-project`가 S3를 사용하게 될 때에는 이 파일을 넓히지 않고, 소비
|
||||
namespace와 ServiceAccount 또는 Pod label을 고정한 별도 정책을 추가한다.
|
||||
|
||||
```bash
|
||||
kubectl kustomize infrastructure/networking/aistor
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- network-policies.yaml
|
||||
@@ -0,0 +1,313 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: aistor-default-deny
|
||||
namespace: aistor
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage-control-plane
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: aistor-allow-dns
|
||||
namespace: aistor
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage-control-plane
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: aistor-allow-kubernetes-api
|
||||
namespace: aistor
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage-control-plane
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 10.43.0.1/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 192.168.0.107/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6443
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: aistor-allow-api-server-admission-webhook
|
||||
namespace: aistor
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage-control-plane
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
aistor.min.io/name: object-store-webhook
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- ipBlock:
|
||||
cidr: 192.168.0.107/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8443
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: aistor-allow-object-store-management
|
||||
namespace: aistor
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage-control-plane
|
||||
spec:
|
||||
podSelector:
|
||||
matchExpressions:
|
||||
- key: aistor.min.io/name
|
||||
operator: In
|
||||
values:
|
||||
- adminjob-operator
|
||||
- object-store-operator
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: object-storage
|
||||
podSelector:
|
||||
matchLabels:
|
||||
aistor.min.io/objectStore: minio-aistor
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 4221
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: object-storage
|
||||
podSelector:
|
||||
matchLabels:
|
||||
aistor.min.io/objectStore: minio-aistor
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
- protocol: TCP
|
||||
port: 4444
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: object-storage-allow-labeled-clients
|
||||
namespace: object-storage
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
platform.hyeonworks.com/aistor-client: "true"
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: object-storage
|
||||
podSelector:
|
||||
matchLabels:
|
||||
aistor.min.io/objectStore: minio-aistor
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
- protocol: TCP
|
||||
port: 9090
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: object-storage-default-deny
|
||||
namespace: object-storage
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: object-storage-allow-dns
|
||||
namespace: object-storage
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: object-storage-allow-kubernetes-api
|
||||
namespace: object-storage
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 10.43.0.1/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 192.168.0.107/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6443
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: object-storage-allow-internal-clients
|
||||
namespace: object-storage
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
aistor.min.io/objectStore: minio-aistor
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
aistor.min.io/objectStore: minio-aistor
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
platform.hyeonworks.com/aistor-client: "true"
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
- protocol: TCP
|
||||
port: 9090
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: object-storage
|
||||
podSelector:
|
||||
matchLabels:
|
||||
aistor.min.io/objectStore: minio-aistor
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
- protocol: TCP
|
||||
port: 9090
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: object-storage-allow-aistor-operator
|
||||
namespace: object-storage
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/component: object-storage
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
aistor.min.io/objectStore: minio-aistor
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: aistor
|
||||
podSelector:
|
||||
matchExpressions:
|
||||
- key: aistor.min.io/name
|
||||
operator: In
|
||||
values:
|
||||
- adminjob-operator
|
||||
- object-store-operator
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
- protocol: TCP
|
||||
port: 4444
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: aistor
|
||||
podSelector:
|
||||
matchLabels:
|
||||
aistor.min.io/name: object-store-operator
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 4221
|
||||
@@ -0,0 +1,309 @@
|
||||
# 호스트 Nginx 전환
|
||||
|
||||
이 디렉터리에는 호스트 수준 Nginx 신뢰 경계의 설정 원본이 있다. 이 파일은
|
||||
Kubernetes 리소스가 아니며 Argo CD에서 조정하지 않는다.
|
||||
2026-07-23 현재 활성 설정은 Gitea와 Keycloak을 함께 proxy하는
|
||||
[learn-services-keycloak.conf](./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-For` chain을 이어 붙이지 않는다.
|
||||
- `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-keycloak.conf)다. 전환 전
|
||||
[learn-services.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를 전환하지 않는다.
|
||||
|
||||
1. Gitea 워크로드, Service 및 Ingress가 Ready 상태다.
|
||||
2. Ingress는 `git.learn.hyeonworks.com`을 사용하며 Kubernetes TLS block이 없다.
|
||||
3. 다음 Traefik 직접 probe가 Gitea health 응답을 반환한다.
|
||||
|
||||
```sh
|
||||
curl --fail-with-body \
|
||||
--header 'Host: git.learn.hyeonworks.com' \
|
||||
http://127.0.0.1:30080/api/healthz
|
||||
```
|
||||
|
||||
4. 별도 LAN 호스트에서 `192.168.0.107:30080`과 `192.168.0.107:30443`의 TCP
|
||||
연결이 모두 거부되거나 timeout되는지 확인한다.
|
||||
|
||||
```sh
|
||||
nc -vz -w 3 192.168.0.107 30080
|
||||
nc -vz -w 3 192.168.0.107 30443
|
||||
```
|
||||
|
||||
TCP 연결이나 HTTP 응답이 하나라도 성공하면 이 차단 조건을 통과하지 못한 것이다. Traefik `404`는
|
||||
포트가 차단된 것이 아니라 정상적으로 도달했다는 증거다. 포트 하나라도
|
||||
도달할 수 있으면 전환을 중지하고 `../traefik/README.md`의 제한 지침을 따른다.
|
||||
|
||||
5. 활성 인증서가 이 파일에 남아 있는 두 hostname을 모두 포함한다.
|
||||
|
||||
```sh
|
||||
sudo certbot certificates
|
||||
```
|
||||
|
||||
6. `sudo nginx -t`와 Nginx 서비스 상태 검사를 통과한다.
|
||||
|
||||
## 최초 Gitea 전환 절차(과거 기록)
|
||||
|
||||
다음 절차는 Keycloak 전환 전 Gitea-only 설정을 처음 적용했을 때의 기록이다.
|
||||
현재 활성 Keycloak 설정에 이 스크립트를 재실행하지 않는다. 당시에는 Argo CD가
|
||||
아니라 의도적인 호스트 작업으로 저장소 루트에서 다음 스크립트를 실행했다.
|
||||
|
||||
```sh
|
||||
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 공개 경로의 수용 기준이 아니다.
|
||||
|
||||
```sh
|
||||
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`여야 한다. 실패 시
|
||||
스크립트가 출력한 정확한 백업 경로를 사용해 다음 순서로 수동 복구한다.
|
||||
|
||||
```sh
|
||||
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차 전환 성공, 공개
|
||||
경로 검증 결과는
|
||||
[중앙 실행 기록](../../../../docs/platform/runbooks/2026-07-23-host-nginx-gitea-cutover.md)에
|
||||
보존한다. 사용자 인증이 필요한 Git clone/push/reclone은 아직 남아 있으므로 그
|
||||
결과도 같은 문서에 추가한다.
|
||||
|
||||
## Keycloak proxy 전환
|
||||
|
||||
### 완료 상태와 선행 조건
|
||||
|
||||
Keycloak 후보는 2026-07-23 활성화했다. 다음 항목은 전환 전에 모두 통과한
|
||||
선행 조건이다.
|
||||
|
||||
1. `keycloak` namespace의 공식 Keycloak Operator와 Server `26.7.0`이 Ready다.
|
||||
2. `hyeonworks` realm의 내부 discovery가 JSON으로 응답하고 issuer가 정확히
|
||||
`https://id.learn.hyeonworks.com/realms/hyeonworks`다.
|
||||
|
||||
```sh
|
||||
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"'
|
||||
```
|
||||
|
||||
3. Traefik `web` entrypoint가 Host Nginx 경로에서 실제로 관측한 한 주소
|
||||
`10.42.0.1/32`만 신뢰한다. `forwardedHeaders.insecure`와 `websecure` trust는
|
||||
없어야 한다.
|
||||
|
||||
```sh
|
||||
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 인자는 다음 한 줄이다.
|
||||
|
||||
```text
|
||||
--entryPoints.web.forwardedHeaders.trustedIPs=10.42.0.1/32
|
||||
```
|
||||
|
||||
4. Keycloak Ingress backend는 `keycloak-service:8080`뿐이며 관리 포트 `9000`은
|
||||
Ingress, NodePort, LoadBalancer와 Host Nginx 후보에 연결되지 않는다.
|
||||
|
||||
```sh
|
||||
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.conf
|
||||
```
|
||||
|
||||
`keycloak-service` 자체가 내부 `ClusterIP`에서 `9000`을 제공하는 것은
|
||||
Operator의 관리 interface 계약이다. 실패 조건은 이 포트를 외부 경로에 연결한
|
||||
Ingress, NodePort, LoadBalancer 또는 Nginx `proxy_pass`가 존재하는 경우다.
|
||||
|
||||
5. 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 작업을 실행했다.
|
||||
|
||||
```sh
|
||||
cd /home/donghyeon/workspace/platform
|
||||
sudo bash scripts/bootstrap/apply-host-nginx-keycloak.sh --execute
|
||||
```
|
||||
|
||||
스크립트가 보여 준 후보·활성 파일과 SHA-256, backup 경로를 확인한 뒤 prompt에
|
||||
정확히 다음을 입력했다.
|
||||
|
||||
```text
|
||||
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와 `hyeonworks` discovery 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 경로를 사용한다.
|
||||
|
||||
```sh
|
||||
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 실행 기록](../../../../docs/platform/runbooks/2026-07-23-keycloak-gitea-oidc-cutover.md)에
|
||||
보존한다.
|
||||
|
||||
## Observability 단계 전환
|
||||
|
||||
Observability 전환은 기존 site 전체를 임의로 재생성하지 않고 다음 네 mode만 사용한다.
|
||||
인자 없는 호출은 공개 Grafana DNS 부재와 source/active SHA-256만 읽는 dry-run이다.
|
||||
|
||||
```sh
|
||||
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는 이 저장소가 자동으로 변경하지 않는다.
|
||||
@@ -0,0 +1,165 @@
|
||||
# Phase 3 candidate for /etc/nginx/sites-available/learn-services.
|
||||
# Host Nginx terminates TLS. Traefik receives HTTP only on loopback NodePort.
|
||||
|
||||
map $http_upgrade $hyeonworks_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server ipv6only=on;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name git.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 512m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name id.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name storage-admin.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host storage-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host storage-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 5g;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name db-admin.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host db-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host db-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name git.learn.hyeonworks.com;
|
||||
return 301 https://git.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name id.learn.hyeonworks.com;
|
||||
return 301 https://id.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name storage-admin.learn.hyeonworks.com;
|
||||
return 301 https://storage-admin.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name db-admin.learn.hyeonworks.com;
|
||||
return 301 https://db-admin.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
# Phase 3 candidate for /etc/nginx/sites-available/learn-services.
|
||||
# Host Nginx terminates TLS. Traefik receives HTTP only on loopback NodePort.
|
||||
|
||||
map $http_upgrade $hyeonworks_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server ipv6only=on;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name git.learn.hyeonworks.com;
|
||||
|
||||
location = /metrics {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 512m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name id.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name storage-admin.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host storage-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host storage-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 5g;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name db-admin.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host db-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host db-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name git.learn.hyeonworks.com;
|
||||
return 301 https://git.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name id.learn.hyeonworks.com;
|
||||
return 301 https://id.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name storage-admin.learn.hyeonworks.com;
|
||||
return 301 https://storage-admin.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name db-admin.learn.hyeonworks.com;
|
||||
return 301 https://db-admin.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name grafana.learn.hyeonworks.com;
|
||||
|
||||
location = /metrics {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:1;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host grafana.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host grafana.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/grafana.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/grafana.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name grafana.learn.hyeonworks.com;
|
||||
return 301 https://grafana.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
# Phase 2 candidate for /etc/nginx/sites-available/learn-services.
|
||||
#
|
||||
# Host Nginx is the only public TLS endpoint. Both virtual hosts forward to the
|
||||
# loopback-only Traefik HTTP NodePort; Keycloak is an OIDC provider, not an
|
||||
# authentication proxy in front of Gitea.
|
||||
|
||||
map $http_upgrade $gitea_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server ipv6only=on;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name git.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $gitea_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
|
||||
client_max_body_size 512m;
|
||||
}
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
listen [::]:443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
server_name id.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_redirect off;
|
||||
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
listen [::]:443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
if ($host = git.learn.hyeonworks.com) {
|
||||
return 301 https://$host$request_uri;
|
||||
} # managed by Certbot
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name git.learn.hyeonworks.com;
|
||||
return 404; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
if ($host = id.learn.hyeonworks.com) {
|
||||
return 301 https://$host$request_uri;
|
||||
} # managed by Certbot
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name id.learn.hyeonworks.com;
|
||||
return 404; # managed by Certbot
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
# Phase 3 candidate for /etc/nginx/sites-available/learn-services.
|
||||
# Host Nginx terminates TLS. Traefik receives HTTP only on loopback NodePort.
|
||||
|
||||
map $http_upgrade $hyeonworks_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server ipv6only=on;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name git.learn.hyeonworks.com;
|
||||
|
||||
location = /metrics {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 512m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name id.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name storage-admin.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host storage-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host storage-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 5g;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name db-admin.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host db-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host db-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name git.learn.hyeonworks.com;
|
||||
return 301 https://git.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name id.learn.hyeonworks.com;
|
||||
return 301 https://id.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name storage-admin.learn.hyeonworks.com;
|
||||
return 301 https://storage-admin.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name db-admin.learn.hyeonworks.com;
|
||||
return 301 https://db-admin.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
# Phase 3 candidate for /etc/nginx/sites-available/learn-services.
|
||||
# Host Nginx terminates TLS. Traefik receives HTTP only on loopback NodePort.
|
||||
|
||||
map $http_upgrade $hyeonworks_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server ipv6only=on;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name git.learn.hyeonworks.com;
|
||||
|
||||
location = /metrics {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 512m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name id.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host id.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name storage-admin.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host storage-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host storage-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 5g;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name db-admin.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host db-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host db-admin.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/storage-admin.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name git.learn.hyeonworks.com;
|
||||
return 301 https://git.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name id.learn.hyeonworks.com;
|
||||
return 301 https://id.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name storage-admin.learn.hyeonworks.com;
|
||||
return 301 https://storage-admin.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name db-admin.learn.hyeonworks.com;
|
||||
return 301 https://db-admin.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name grafana.learn.hyeonworks.com;
|
||||
|
||||
location = /metrics {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
allow 192.168.0.0/24;
|
||||
allow 100.64.0.0/10;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host grafana.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host grafana.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $hyeonworks_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/grafana.learn.hyeonworks.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/grafana.learn.hyeonworks.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name grafana.learn.hyeonworks.com;
|
||||
return 301 https://grafana.learn.hyeonworks.com$request_uri;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# Candidate configuration for /etc/nginx/sites-available/learn-services.
|
||||
#
|
||||
# This file is source-controlled configuration, not the active host configuration.
|
||||
# Review the accompanying README before installing it.
|
||||
|
||||
map $http_upgrade $gitea_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# Do not let an unknown TLS hostname fall through to the first named virtual
|
||||
# host. Nginx can reject the handshake without presenting either certificate.
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server ipv6only=on;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name git.learn.hyeonworks.com;
|
||||
|
||||
# TLS terminates here. The hop to the k3s Traefik web NodePort is HTTP.
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:30080;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# Replace all externally supplied forwarding headers at the trust boundary.
|
||||
proxy_set_header Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Host git.learn.hyeonworks.com;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
# Keep Git operations and WebSocket-capable endpoints streaming.
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $gitea_connection_upgrade;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_redirect off;
|
||||
|
||||
client_max_body_size 512m;
|
||||
}
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
listen [::]:443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
# Keycloak is intentionally left on its current static response until its phase.
|
||||
server {
|
||||
server_name id.learn.hyeonworks.com;
|
||||
|
||||
location / {
|
||||
default_type text/plain;
|
||||
return 200 "Keycloak domain reached Nginx successfully\n";
|
||||
}
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
listen [::]:443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/git.learn.hyeonworks.com/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/git.learn.hyeonworks.com/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
if ($host = git.learn.hyeonworks.com) {
|
||||
return 301 https://$host$request_uri;
|
||||
} # managed by Certbot
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name git.learn.hyeonworks.com;
|
||||
return 404; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
if ($host = id.learn.hyeonworks.com) {
|
||||
return 301 https://$host$request_uri;
|
||||
} # managed by Certbot
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name id.learn.hyeonworks.com;
|
||||
return 404; # managed by Certbot
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
/usr/sbin/nginx -t
|
||||
/usr/bin/systemctl reload nginx
|
||||
@@ -0,0 +1,53 @@
|
||||
# 호스트 Split DNS
|
||||
|
||||
두 dnsmasq 프로세스가 서로 다른 주소에만 바인딩됩니다.
|
||||
|
||||
- LAN: 192.168.0.107:53
|
||||
- Tailscale: 100.92.240.34:53
|
||||
|
||||
공유기 DHCP DNS에는 192.168.0.107을 지정하고, Tailscale 관리 화면에는
|
||||
learn.hyeonworks.com 제한 nameserver로 100.92.240.34를 등록합니다.
|
||||
이 두 관리 화면 변경은 자동화하지 않습니다.
|
||||
|
||||
현재 gateway `192.168.0.1`의 MAC OUI와 관리 페이지는 TP-Link 계열이다.
|
||||
dnsmasq 실제 적용이 성공한 뒤 다음 순서로 설정한다.
|
||||
|
||||
## TP-Link LAN
|
||||
|
||||
1. LAN 단말에서 http://192.168.0.1/ 에 로그인한다.
|
||||
2. `Advanced > Network > DHCP Server`에 DNS 필드가 있으면 Primary DNS를
|
||||
`192.168.0.107`로 지정하고 Secondary DNS는 비워 둔다.
|
||||
3. 해당 메뉴에 DNS 필드가 없으면 `Advanced > Network > Internet`에서
|
||||
`Use the following DNS addresses`를 선택하고 Primary DNS를
|
||||
`192.168.0.107`로 지정한다.
|
||||
4. 저장한 뒤 공유기를 재부팅하고 단말의 Wi-Fi를 다시 연결해 DHCP lease를
|
||||
갱신한다.
|
||||
|
||||
공개 DNS를 Secondary에 넣으면 단말이 private resolver를 우회할 수 있으므로
|
||||
사용하지 않는다. 정확한 메뉴는 TP-Link firmware에 따라 둘 중 하나다.
|
||||
|
||||
## Tailscale
|
||||
|
||||
1. https://login.tailscale.com/admin/dns 에 로그인한다.
|
||||
2. `Nameservers > Add nameserver > Custom`을 선택한다.
|
||||
3. nameserver에 `100.92.240.34`를 입력한다.
|
||||
4. `Restrict to search domain`을 켜고 `learn.hyeonworks.com`을 입력해 저장한다.
|
||||
5. 단말에서 Tailscale DNS 수락을 켜고 다시 연결한다.
|
||||
|
||||
검증:
|
||||
|
||||
nslookup git.learn.hyeonworks.com
|
||||
nslookup id.learn.hyeonworks.com
|
||||
nslookup storage-admin.learn.hyeonworks.com
|
||||
nslookup db-admin.learn.hyeonworks.com
|
||||
nslookup grafana.learn.hyeonworks.com
|
||||
|
||||
LAN에서는 다섯 이름이 `192.168.0.107`, Tailscale 경로에서는
|
||||
`100.92.240.34`로 응답해야 한다.
|
||||
|
||||
공식 참고:
|
||||
|
||||
- TP-Link DNS 설정: https://www.tp-link.com/ca/support/faq/1712/
|
||||
- Tailscale split DNS: https://tailscale.com/learn/why-split-dns
|
||||
|
||||
공개 DNS에는 storage-admin, db-admin, grafana A/AAAA 레코드를 만들지 않습니다.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Hyeonworks LAN 전용 dnsmasq 인스턴스
|
||||
port=53
|
||||
listen-address=192.168.0.107
|
||||
bind-interfaces
|
||||
no-hosts
|
||||
no-resolv
|
||||
server=1.1.1.1
|
||||
server=1.0.0.1
|
||||
cache-size=1000
|
||||
domain-needed
|
||||
bogus-priv
|
||||
address=/git.learn.hyeonworks.com/192.168.0.107
|
||||
address=/id.learn.hyeonworks.com/192.168.0.107
|
||||
address=/storage-admin.learn.hyeonworks.com/192.168.0.107
|
||||
address=/db-admin.learn.hyeonworks.com/192.168.0.107
|
||||
address=/grafana.learn.hyeonworks.com/192.168.0.107
|
||||
@@ -0,0 +1,16 @@
|
||||
# Hyeonworks Tailscale 전용 dnsmasq 인스턴스
|
||||
port=53
|
||||
listen-address=100.92.240.34
|
||||
bind-interfaces
|
||||
no-hosts
|
||||
no-resolv
|
||||
server=1.1.1.1
|
||||
server=1.0.0.1
|
||||
cache-size=1000
|
||||
domain-needed
|
||||
bogus-priv
|
||||
address=/git.learn.hyeonworks.com/100.92.240.34
|
||||
address=/id.learn.hyeonworks.com/100.92.240.34
|
||||
address=/storage-admin.learn.hyeonworks.com/100.92.240.34
|
||||
address=/db-admin.learn.hyeonworks.com/100.92.240.34
|
||||
address=/grafana.learn.hyeonworks.com/100.92.240.34
|
||||
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=Hyeonworks LAN private DNS
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/sbin/dnsmasq --keep-in-foreground --user=dnsmasq --group=nogroup --conf-file=/etc/dnsmasq-hyeonworks/lan.conf --pid-file=/run/hyeonworks-dnsmasq-lan.pid
|
||||
Restart=on-failure
|
||||
RestartSec=2s
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=Hyeonworks Tailscale private DNS
|
||||
After=network-online.target tailscaled.service
|
||||
Wants=network-online.target tailscaled.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/sbin/dnsmasq --keep-in-foreground --user=dnsmasq --group=nogroup --conf-file=/etc/dnsmasq-hyeonworks/tailscale.conf --pid-file=/run/hyeonworks-dnsmasq-tailscale.pid
|
||||
Restart=on-failure
|
||||
RestartSec=2s
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,8 @@
|
||||
# k3s 내부 Split DNS
|
||||
|
||||
k3s CoreDNS의 기존 /etc/coredns/custom/*.server 확장점을 이용해
|
||||
learn.hyeonworks.com의 다섯 서비스 이름을 호스트 Nginx가 있는
|
||||
192.168.0.107로 해석합니다.
|
||||
|
||||
이 구성이 적용되고 Pod 검증이 끝나면 Gitea의 임시 hostAliases는
|
||||
필요하지 않습니다. 다른 DNS 영역은 기존 CoreDNS 전달 경로를 유지합니다.
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: coredns-custom
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: coredns
|
||||
app.kubernetes.io/component: private-dns
|
||||
app.kubernetes.io/part-of: platform
|
||||
data:
|
||||
learn-hyeonworks.server: |
|
||||
learn.hyeonworks.com:53 {
|
||||
errors
|
||||
cache 30
|
||||
hosts {
|
||||
192.168.0.107 git.learn.hyeonworks.com
|
||||
192.168.0.107 id.learn.hyeonworks.com
|
||||
192.168.0.107 storage-admin.learn.hyeonworks.com
|
||||
192.168.0.107 db-admin.learn.hyeonworks.com
|
||||
192.168.0.107 grafana.learn.hyeonworks.com
|
||||
fallthrough
|
||||
}
|
||||
forward . /etc/resolv.conf
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- coredns-custom.yaml
|
||||
@@ -0,0 +1,287 @@
|
||||
# Traefik 경계 계약과 적용 기록
|
||||
|
||||
Traefik은 k3s가 관리하는 클러스터 내부 Ingress Controller다. 공개 요청은 반드시
|
||||
Host Nginx에서 TLS를 종료한 뒤 loopback NodePort를 통해 Traefik의 `web`
|
||||
entrypoint로 들어온다. 이 저장소는 두 번째 Ingress Controller를 설치하지 않으며,
|
||||
애플리케이션 Ingress에 클러스터 내부 TLS를 중복 구성하지 않는다.
|
||||
|
||||
2026-07-23 현재 `kube-system/traefik` `HelmChartConfig`에는 trust overlay가 실제로
|
||||
적용돼 있다. Host Nginx 경유 관측에서 확인한 `ClientHost` `10.42.0.1` 한 주소만
|
||||
`10.42.0.1/32`로 신뢰하며, Gitea의 site manifest도 외부 HTTPS URL을 생성한다.
|
||||
|
||||
## 현재 live 상태
|
||||
|
||||
| 항목 | 확인된 값 |
|
||||
|---|---|
|
||||
| k3s Traefik Chart | `40.1.3+up40.1.0` |
|
||||
| Traefik 이미지 | `v3.7.4` |
|
||||
| `HelmChartConfig` | trust overlay와 일치, live 적용됨 |
|
||||
| Service 유형 | `NodePort`, `externalTrafficPolicy: Cluster` |
|
||||
| `web` | Service `80`, NodePort `30080` |
|
||||
| `websecure` | Service `443`, NodePort `30443` |
|
||||
| NodePort bind 범위 | `127.0.0.0/8` |
|
||||
| JSON access log | 활성화, request header 기록 제외 |
|
||||
| 관측 `ClientHost` | `10.42.0.1` |
|
||||
| `web` trusted CIDR | `10.42.0.1/32` |
|
||||
| `websecure` forwarded-header trust | 없음 |
|
||||
| `forwardedHeaders.insecure` | 없음 |
|
||||
| Gitea site manifest | `start_url`과 icon URL 모두 `https://git.learn.hyeonworks.com/` 기준 |
|
||||
| 실제 ingress 경로 | `Host Nginx :443 -> 127.0.0.1:30080 -> Traefik web` |
|
||||
|
||||
다음 명령으로 변할 수 있는 live 상태를 다시 확인한다.
|
||||
|
||||
```sh
|
||||
kubectl -n kube-system get helmchartconfig.helm.cattle.io/traefik
|
||||
|
||||
kubectl -n kube-system get service/traefik \
|
||||
-o custom-columns='NAME:.metadata.name,TYPE:.spec.type,PORTS:.spec.ports[*].port,NODEPORTS:.spec.ports[*].nodePort'
|
||||
|
||||
kubectl -n kube-system get deployment/traefik -o json |
|
||||
jq -r '.spec.template.spec.containers[] | select(.name == "traefik") | .args[]'
|
||||
```
|
||||
|
||||
`web=30080`, `websecure=30443`, Service `NodePort` 중 하나라도 다르면 Host Nginx를
|
||||
새 포트로 임의 변경하지 말고 중지한다. 선언과 live 상태가 왜 달라졌는지 먼저
|
||||
확인한다.
|
||||
|
||||
## 트래픽과 노출 경계
|
||||
|
||||
- 애플리케이션 Ingress가 hostname에서 Service로 이어지는 routing을 소유한다.
|
||||
- 모든 Ingress는 `spec.ingressClassName: traefik`과 `web` entrypoint를 명시한다.
|
||||
- 공개 TLS는 Host Nginx가 종료하므로 애플리케이션 Ingress에 `spec.tls`를 넣지 않는다.
|
||||
- `websecure` NodePort `30443`은 Service 계약상 고정하지만 현재 Host Nginx upstream은
|
||||
사용하지 않는다. 이 entrypoint에는 forwarded-header trust도 설정하지 않는다.
|
||||
- Traefik Dashboard와 관리 endpoint는 공개하지 않는다.
|
||||
- k3s drop-in의 `nodeport-addresses=127.0.0.0/8`이 LAN에서 NodePort에 직접
|
||||
접근하는 우회 경로를 차단한다.
|
||||
|
||||
서버 node IP와 별도 LAN 클라이언트에서는 다음 연결이 거부되거나 timeout이어야
|
||||
한다. Traefik `404`도 TCP 연결에 성공했다는 뜻이므로 실패다.
|
||||
|
||||
```sh
|
||||
nc -vz -w 3 192.168.0.107 30080
|
||||
nc -vz -w 3 192.168.0.107 30443
|
||||
```
|
||||
|
||||
반대로 서버 loopback에서는 두 포트가 listening 상태여야 하며 Host 기반 Gitea
|
||||
health가 통과해야 한다.
|
||||
|
||||
```sh
|
||||
nc -vz -w 3 127.0.0.1 30080
|
||||
nc -vz -w 3 127.0.0.1 30443
|
||||
|
||||
curl --fail-with-body \
|
||||
--header 'Host: git.learn.hyeonworks.com' \
|
||||
http://127.0.0.1:30080/api/healthz
|
||||
```
|
||||
|
||||
UFW는 현재 inactive다. 인터넷 측 고포트 차단 여부는 LAN 결과에서 추론하지 않고
|
||||
router 규칙 또는 별도 외부망 검사로 확인한다.
|
||||
|
||||
## 선언 구조와 각 overlay의 역할
|
||||
|
||||
```text
|
||||
infrastructure/networking/traefik/
|
||||
├── base/
|
||||
│ └── helm-chart-config.yaml
|
||||
├── overlays/
|
||||
│ ├── baseline/
|
||||
│ │ └── service-boundary-only-patch.yaml
|
||||
│ ├── observe/
|
||||
│ └── trust/
|
||||
│ └── trusted-proxy-cidr-patch.yaml
|
||||
└── scripts/
|
||||
├── apply-observe.sh
|
||||
├── observe-client-host.sh
|
||||
├── apply-trust.sh
|
||||
├── rollback-to-observe.sh
|
||||
└── validate.sh
|
||||
```
|
||||
|
||||
세 overlay는 모두 Service `NodePort`, `externalTrafficPolicy: Cluster`와
|
||||
`30080/30443`을 명시적으로 소유한다.
|
||||
|
||||
- `baseline`: Service 경계만 남긴다. access log와 forwarded-header trust는 없다.
|
||||
- `observe`: Service 경계와 header를 버리는 JSON access log를 적용한다. trust는 없다.
|
||||
- `trust`: observe 설정에 `web.forwardedHeaders.trustedIPs=10.42.0.1/32`만 추가한다.
|
||||
|
||||
루트 `kustomization.yaml`은 의도적으로 안전한 `observe` overlay를 가리킨다. 현재
|
||||
live 상태는 `trust`이므로 루트에 단순히 `kubectl apply -k`를 실행하면 trust 제거를
|
||||
요청하게 된다. 상태 전환은 아래 guarded script와 정확한 overlay를 사용한다.
|
||||
Chart 원본이나 k3s가 소유한 `HelmChart`는 직접 수정하지 않는다.
|
||||
|
||||
## 첫 observe 적용 실패와 복구
|
||||
|
||||
첫 observe 적용 때 `HelmChartConfig`에는 access log만 있고 Traefik Service values가
|
||||
없었다. k3s Helm Controller가 전체 Chart를 기본값으로 다시 조정하면서 다음 drift가
|
||||
발생했다.
|
||||
|
||||
```text
|
||||
기존: NodePort web=30080, websecure=30443
|
||||
변경: LoadBalancer web=31251, websecure=30997
|
||||
```
|
||||
|
||||
이어진 loopback listener 검사가 실패했다. 당시 실패 처리도 새
|
||||
`HelmChartConfig`를 삭제했을 뿐, desired state에 없던 수동 Service spec은 복원하지
|
||||
못했다. Gitea·PostgreSQL·PV/PVC는 건드리지 않고 Traefik Service만 다음 명령으로
|
||||
즉시 원래 경계에 복구했다.
|
||||
|
||||
```sh
|
||||
kubectl -n kube-system patch service traefik \
|
||||
--type=merge \
|
||||
--patch '{"spec":{"type":"NodePort","externalTrafficPolicy":"Cluster","ports":[{"name":"web","port":80,"protocol":"TCP","targetPort":"web","nodePort":30080},{"name":"websecure","port":443,"protocol":"TCP","targetPort":"websecure","nodePort":30443}]}}'
|
||||
```
|
||||
|
||||
그 뒤 다음을 영구 보완했다.
|
||||
|
||||
- `base`, `baseline`, `observe`, `trust`가 Service type과 정확한 NodePort를 선언한다.
|
||||
- `baseline` overlay를 추가해 access log나 trust 없이도 NodePort desired state를
|
||||
유지한다.
|
||||
- observe 실패 시 `HelmChartConfig`를 삭제하지 않고 baseline을 적용한다.
|
||||
- trust 실패 또는 표준 trust 롤백 시 observe를 적용한다.
|
||||
- rollout 뒤 NodePort listener와 Gitea health가 수렴할 때까지 bounded wait를 한다.
|
||||
- 검증기는 세 overlay에서 LoadBalancer 부재와 `30080/30443`을 강제한다.
|
||||
|
||||
따라서 `HelmChartConfig` 삭제는 더 이상 롤백 방법이 아니다. 삭제하면 Chart 기본값이
|
||||
다시 Service를 소유해 같은 drift를 재발시킬 수 있다.
|
||||
|
||||
## 전달 헤더 최소 신뢰 적용 결과
|
||||
|
||||
Host Nginx는 외부 요청의 기존 forwarded chain을 이어 붙이지 않고 신뢰 경계에서
|
||||
다음 값을 새로 만든다.
|
||||
|
||||
- `Host`는 선택한 공개 hostname으로 고정한다.
|
||||
- `X-Real-IP`와 `X-Forwarded-For`는 Nginx가 실제로 본 client address로 교체한다.
|
||||
- `X-Forwarded-Proto`는 `https`, `X-Forwarded-Port`는 `443`으로 고정한다.
|
||||
|
||||
observe 단계에서 다음 probe가 Host Nginx를 반드시 통과하는 고유 요청을 만들고
|
||||
Traefik JSON access log의 한 router 기록만 읽었다. request header와 자격 증명은
|
||||
로그에 남기지 않았다.
|
||||
|
||||
```sh
|
||||
bash infrastructure/networking/traefik/scripts/observe-client-host.sh
|
||||
```
|
||||
|
||||
확인 결과는 다음과 같다.
|
||||
|
||||
```text
|
||||
ClientHost: 10.42.0.1
|
||||
Minimum trusted CIDR: 10.42.0.1/32
|
||||
```
|
||||
|
||||
Pod CIDR 전체, loopback 전체 또는 LAN CIDR을 추정해 넓히지 않고 이 한 주소만 trust
|
||||
overlay에 기록했다. 적용 명령과 승인 문자열은 다음과 같았다.
|
||||
|
||||
```sh
|
||||
bash infrastructure/networking/traefik/scripts/apply-trust.sh \
|
||||
--observed-client-host '10.42.0.1' \
|
||||
--execute
|
||||
```
|
||||
|
||||
```text
|
||||
APPLY default TRUST 10.42.0.1/32
|
||||
```
|
||||
|
||||
현재 runtime에는 다음 trust 인자 하나만 존재한다.
|
||||
|
||||
```text
|
||||
--entryPoints.web.forwardedHeaders.trustedIPs=10.42.0.1/32
|
||||
```
|
||||
|
||||
`entryPoints.websecure.forwardedHeaders.*`와 `forwardedHeaders.insecure` 인자는 없다.
|
||||
적용 후 `/assets/site-manifest.json`의 `start_url`과 두 icon URL이 모두 HTTPS로
|
||||
확인됐고 Gitea health의 status·database·cache 검사도 통과했다.
|
||||
|
||||
## 검증과 상태 전환
|
||||
|
||||
소스와 세 overlay의 정적 계약은 다음 명령으로 검증한다.
|
||||
|
||||
```sh
|
||||
cd /home/donghyeon/workspace/platform
|
||||
bash infrastructure/networking/traefik/scripts/validate.sh
|
||||
```
|
||||
|
||||
검증기는 다음 조건을 강제한다.
|
||||
|
||||
- 세 overlay의 Service가 `NodePort`, `externalTrafficPolicy: Cluster`,
|
||||
`30080/30443`을 유지한다.
|
||||
- observe와 trust access log는 JSON이고 request header를 기록하지 않는다.
|
||||
- trust CIDR은 관측한 단일 host `/32` 또는 `/128` 형식이다.
|
||||
- `forwardedHeaders.insecure`, `websecure` trust, `LoadBalancer`가 없다.
|
||||
|
||||
새 설치처럼 `HelmChartConfig`가 없거나 이미 observe 상태인 경우에는 다음 guarded
|
||||
script로 observe 구성을 확인하거나 적용한다.
|
||||
|
||||
```sh
|
||||
bash infrastructure/networking/traefik/scripts/apply-observe.sh --execute
|
||||
# 승인: APPLY <현재-context> OBSERVE
|
||||
```
|
||||
|
||||
현재 live trust에서 다시 관측하려면 먼저 아래 표준 롤백으로 observe를 적용한 뒤
|
||||
probe를 실행한다. trust 상태에서 `apply-observe.sh`를 바로 실행하지 않는다.
|
||||
|
||||
```sh
|
||||
bash infrastructure/networking/traefik/scripts/rollback-to-observe.sh --execute
|
||||
bash infrastructure/networking/traefik/scripts/observe-client-host.sh
|
||||
```
|
||||
|
||||
관측값이 달라지면 기존 CIDR을 넓히지 말고 trust patch를 exact host CIDR로 갱신한 뒤
|
||||
`apply-trust.sh`를 실행한다. Chart, 이미지, context, API server, NodePort 경계 또는
|
||||
재관측 값이 기대와 다르면 스크립트가 적용을 중단한다.
|
||||
|
||||
### 롤백
|
||||
|
||||
trust만 제거하고 JSON access log를 남기는 표준 롤백은 observe overlay를 적용한다.
|
||||
|
||||
```sh
|
||||
bash infrastructure/networking/traefik/scripts/rollback-to-observe.sh --execute
|
||||
# 승인: ROLLBACK <현재-context> OBSERVE
|
||||
```
|
||||
|
||||
observe 적용 자체가 실패하면 `apply-observe.sh`가 NodePort-only baseline overlay를
|
||||
적용한다. access log와 trust를 모두 제거해야 하는 명시적 유지보수에서는 live
|
||||
context와 대상 overlay를 재확인한 뒤 baseline을 적용한다.
|
||||
|
||||
```sh
|
||||
kubectl apply --kustomize \
|
||||
infrastructure/networking/traefik/overlays/baseline
|
||||
```
|
||||
|
||||
어느 경우에도 `HelmChartConfig`를 삭제해 롤백하지 않는다. baseline 또는 observe를
|
||||
적용해 Service `30080/30443`을 계속 desired state로 남긴다.
|
||||
|
||||
k3s·kube-proxy·CNI·Service traffic policy나 Host Nginx 경로를 바꾸면
|
||||
`ClientHost`가 달라질 수 있다. 이때는 observe로 돌아가 다시 관측하고 정확한 한
|
||||
주소만 trust한다.
|
||||
|
||||
## 종단 간 인수 조건
|
||||
|
||||
다음 로컬 검사는 Host Nginx와 Traefik을 함께 통과해야 한다.
|
||||
|
||||
```sh
|
||||
curl --fail-with-body \
|
||||
--resolve git.learn.hyeonworks.com:443:127.0.0.1 \
|
||||
https://git.learn.hyeonworks.com/api/healthz
|
||||
|
||||
curl --fail-with-body \
|
||||
--resolve git.learn.hyeonworks.com:443:127.0.0.1 \
|
||||
https://git.learn.hyeonworks.com/assets/site-manifest.json |
|
||||
jq -e '
|
||||
.start_url == "https://git.learn.hyeonworks.com/" and
|
||||
([.icons[].src | startswith("https://git.learn.hyeonworks.com/")] | all)
|
||||
'
|
||||
```
|
||||
|
||||
별도 LAN 클라이언트에서 `192.168.0.107:30080/30443`이 거부되는지 다시 확인하고,
|
||||
독립 외부망에서는 공개 HTTP→HTTPS redirect와 두 서비스의 HTTPS 응답을 검사한다.
|
||||
서버에서 공인 FQDN으로 향하는 NAT hairpin timeout만으로 공개 실패를 판정하지 않는다.
|
||||
|
||||
구현 근거는 [k3s HelmChartConfig](https://docs.k3s.io/helm),
|
||||
[k3s 내장 Traefik](https://docs.k3s.io/networking/networking-services),
|
||||
[Traefik forwarded headers](https://doc.traefik.io/traefik/reference/install-configuration/entrypoints/),
|
||||
[Traefik access log](https://doc.traefik.io/traefik/observe/logs-and-access-logs/),
|
||||
[Traefik Chart 40.1.0 values](https://github.com/traefik/traefik-helm-chart/blob/v40.1.0/traefik/values.yaml)다.
|
||||
|
||||
첫 실패, 수동 복구, 영구 보완, 관측값과 trust 적용의 전체 명령·출력은
|
||||
[중앙 실행 기록](../../../../docs/platform/runbooks/2026-07-23-traefik-forwarded-header-trust-boundary.md)에
|
||||
보존한다.
|
||||
@@ -0,0 +1,45 @@
|
||||
apiVersion: helm.cattle.io/v1
|
||||
kind: HelmChartConfig
|
||||
metadata:
|
||||
name: traefik
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/part-of: platform
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
spec:
|
||||
failurePolicy: abort
|
||||
valuesContent: |-
|
||||
deployment:
|
||||
podAnnotations:
|
||||
prometheus.io/path: /metrics
|
||||
prometheus.io/port: "9100"
|
||||
prometheus.io/scrape: "true"
|
||||
service:
|
||||
spec:
|
||||
type: NodePort
|
||||
externalTrafficPolicy: Cluster
|
||||
ports:
|
||||
web:
|
||||
nodePort: 30080
|
||||
websecure:
|
||||
nodePort: 30443
|
||||
metrics:
|
||||
prometheus:
|
||||
service:
|
||||
enabled: true
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
additionalLabels:
|
||||
observability.hyeonworks.com/instance: home
|
||||
jobLabel: app.kubernetes.io/name
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
logs:
|
||||
access:
|
||||
enabled: true
|
||||
format: json
|
||||
fields:
|
||||
general:
|
||||
defaultmode: keep
|
||||
headers:
|
||||
defaultmode: drop
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- helm-chart-config.yaml
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- overlays/observe
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
patches:
|
||||
- path: service-boundary-only-patch.yaml
|
||||
target:
|
||||
group: helm.cattle.io
|
||||
version: v1
|
||||
kind: HelmChartConfig
|
||||
name: traefik
|
||||
namespace: kube-system
|
||||
@@ -0,0 +1,28 @@
|
||||
- op: replace
|
||||
path: /spec/valuesContent
|
||||
value: |-
|
||||
deployment:
|
||||
podAnnotations:
|
||||
prometheus.io/path: /metrics
|
||||
prometheus.io/port: "9100"
|
||||
prometheus.io/scrape: "true"
|
||||
service:
|
||||
spec:
|
||||
type: NodePort
|
||||
externalTrafficPolicy: Cluster
|
||||
ports:
|
||||
web:
|
||||
nodePort: 30080
|
||||
websecure:
|
||||
nodePort: 30443
|
||||
metrics:
|
||||
prometheus:
|
||||
service:
|
||||
enabled: true
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
additionalLabels:
|
||||
observability.hyeonworks.com/instance: home
|
||||
jobLabel: app.kubernetes.io/name
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
patches:
|
||||
- path: trusted-proxy-cidr-patch.yaml
|
||||
target:
|
||||
group: helm.cattle.io
|
||||
version: v1
|
||||
kind: HelmChartConfig
|
||||
name: traefik
|
||||
namespace: kube-system
|
||||
@@ -0,0 +1,41 @@
|
||||
- op: replace
|
||||
path: /spec/valuesContent
|
||||
value: |-
|
||||
deployment:
|
||||
podAnnotations:
|
||||
prometheus.io/path: /metrics
|
||||
prometheus.io/port: "9100"
|
||||
prometheus.io/scrape: "true"
|
||||
service:
|
||||
spec:
|
||||
type: NodePort
|
||||
externalTrafficPolicy: Cluster
|
||||
ports:
|
||||
web:
|
||||
nodePort: 30080
|
||||
forwardedHeaders:
|
||||
trustedIPs:
|
||||
# 2026-07-23 Host Nginx 경유 probe에서 관측한 Traefik ClientHost이다.
|
||||
- "10.42.0.1/32"
|
||||
websecure:
|
||||
nodePort: 30443
|
||||
metrics:
|
||||
prometheus:
|
||||
service:
|
||||
enabled: true
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
additionalLabels:
|
||||
observability.hyeonworks.com/instance: home
|
||||
jobLabel: app.kubernetes.io/name
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
logs:
|
||||
access:
|
||||
enabled: true
|
||||
format: json
|
||||
fields:
|
||||
general:
|
||||
defaultmode: keep
|
||||
headers:
|
||||
defaultmode: drop
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
set +x
|
||||
umask 077
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck source=common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: bash infrastructure/networking/traefik/scripts/apply-observe.sh --execute
|
||||
|
||||
Applies JSON access logging to the k3s-managed Traefik HelmChartConfig.
|
||||
It does not trust any forwarded header. The existing loopback-only
|
||||
NodePort 30080/30443 boundary is declared explicitly so Helm reconciliation
|
||||
cannot replace it with chart defaults.
|
||||
USAGE
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "--execute" && "$#" -eq 1 ]] || {
|
||||
usage
|
||||
exit 2
|
||||
}
|
||||
|
||||
require_commands kubectl jq rg curl nc python3 awk cmp find bash
|
||||
|
||||
bash "${SCRIPT_DIR}/validate.sh"
|
||||
|
||||
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-observe.XXXXXX")"
|
||||
rollback_required=false
|
||||
selected_context=""
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
|
||||
trap - EXIT
|
||||
if [[ "$exit_code" -ne 0 && "$rollback_required" == "true" ]]; then
|
||||
printf '\nROLLBACK: restoring the durable NodePort-only baseline.\n' >&2
|
||||
set +e
|
||||
kubectl --context "$selected_context" apply --kustomize "$BASELINE_OVERLAY"
|
||||
wait_for_runtime baseline
|
||||
kubectl --context "$selected_context" --namespace kube-system \
|
||||
rollout status deployment/traefik --timeout=5m
|
||||
wait_for_nodeport_boundary_and_health
|
||||
printf 'ROLLBACK complete. NodePort 30080/30443 remains pinned.\n' >&2
|
||||
set -e
|
||||
fi
|
||||
|
||||
case "$work_dir" in
|
||||
/tmp/platform-traefik-observe.*|"${TMPDIR:-/tmp}"/platform-traefik-observe.*)
|
||||
rm -rf -- "$work_dir"
|
||||
;;
|
||||
*)
|
||||
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
|
||||
"$work_dir" >&2
|
||||
;;
|
||||
esac
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
observe_render="${work_dir}/observe.yaml"
|
||||
render_overlay "$OBSERVE_OVERLAY" "$observe_render"
|
||||
|
||||
assert_live_baseline
|
||||
assert_nodeport_boundary_and_health
|
||||
|
||||
selected_context="$(current_context)"
|
||||
selected_api_server="$(current_api_server)"
|
||||
printf '\nKubernetes context: %s\nAPI server: %s\nTarget node: %s\n' \
|
||||
"$selected_context" "$selected_api_server" "$TARGET_NODE"
|
||||
|
||||
already_applied=false
|
||||
if kubectl --namespace kube-system \
|
||||
get helmchartconfig.helm.cattle.io traefik >/dev/null 2>&1; then
|
||||
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
|
||||
"${work_dir}/live-observe-compare.yaml"
|
||||
already_applied=true
|
||||
printf 'The live HelmChartConfig already matches the observation overlay.\n'
|
||||
fi
|
||||
|
||||
printf 'Type APPLY %s OBSERVE to enable JSON access logs: ' "$selected_context"
|
||||
read -r confirmation
|
||||
[[ "$confirmation" == "APPLY ${selected_context} OBSERVE" ]] || \
|
||||
fail "cancelled"
|
||||
|
||||
[[ "$(current_context)" == "$selected_context" ]] || \
|
||||
fail "kubectl context changed after confirmation"
|
||||
[[ "$(current_api_server)" == "$selected_api_server" ]] || \
|
||||
fail "Kubernetes API server changed after confirmation"
|
||||
assert_live_baseline
|
||||
assert_nodeport_boundary_and_health
|
||||
|
||||
if [[ "$already_applied" == "false" ]]; then
|
||||
rollback_required=true
|
||||
kubectl apply --filename "$observe_render"
|
||||
fi
|
||||
|
||||
wait_for_runtime observe
|
||||
kubectl --namespace kube-system rollout status deployment/traefik --timeout=5m
|
||||
assert_runtime observe
|
||||
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
|
||||
"${work_dir}/post-apply-observe.yaml"
|
||||
assert_live_baseline
|
||||
wait_for_nodeport_boundary_and_health
|
||||
|
||||
manifest_start_url="$(
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--resolve "${GITEA_HOST}:443:127.0.0.1" \
|
||||
"$GITEA_MANIFEST_URL" |
|
||||
jq --raw-output '.start_url'
|
||||
)"
|
||||
|
||||
rollback_required=false
|
||||
printf '\nOBSERVATION PHASE READY\n'
|
||||
printf 'Traefik JSON access logging: enabled\n'
|
||||
printf 'forwardedHeaders trust: absent\n'
|
||||
printf 'Current Gitea manifest start_url: %s\n' "$manifest_start_url"
|
||||
printf 'Next: bash %s/observe-client-host.sh\n' "$SCRIPT_DIR"
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
set +x
|
||||
umask 077
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck source=common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
bash infrastructure/networking/traefik/scripts/apply-trust.sh \
|
||||
--observed-client-host <IP> --execute
|
||||
|
||||
The IP must be the ClientHost printed by observe-client-host.sh. The trust
|
||||
overlay must already contain that exact IP as /32 (IPv4) or /128 (IPv6).
|
||||
USAGE
|
||||
}
|
||||
|
||||
[[ "$#" -eq 3 && "$1" == "--observed-client-host" && "$3" == "--execute" ]] || {
|
||||
usage
|
||||
exit 2
|
||||
}
|
||||
|
||||
readonly REVIEWED_CLIENT_HOST="$2"
|
||||
|
||||
require_commands kubectl jq rg curl nc python3 awk sort date cmp find bash
|
||||
|
||||
reviewed_cidr="$(host_to_exact_cidr "$REVIEWED_CLIENT_HOST")"
|
||||
declared_cidr="$(source_trusted_proxy_cidr)"
|
||||
declared_cidr="$(normalize_exact_host_cidr "$declared_cidr")"
|
||||
|
||||
[[ "$declared_cidr" != "$SENTINEL_TRUSTED_PROXY_CIDR" ]] || \
|
||||
fail "trust overlay still contains the non-routable sentinel CIDR"
|
||||
[[ "$declared_cidr" == "$reviewed_cidr" ]] || \
|
||||
fail "declared CIDR ${declared_cidr} does not match ClientHost ${REVIEWED_CLIENT_HOST}"
|
||||
|
||||
bash "${SCRIPT_DIR}/validate.sh"
|
||||
|
||||
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-trust.XXXXXX")"
|
||||
rollback_required=false
|
||||
selected_context=""
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
|
||||
trap - EXIT
|
||||
if [[ "$exit_code" -ne 0 && "$rollback_required" == "true" ]]; then
|
||||
printf '\nROLLBACK: restoring the access-log-only observation overlay.\n' >&2
|
||||
set +e
|
||||
kubectl --context "$selected_context" apply --kustomize "$OBSERVE_OVERLAY"
|
||||
wait_for_runtime observe
|
||||
kubectl --context "$selected_context" --namespace kube-system \
|
||||
rollout status deployment/traefik --timeout=5m
|
||||
wait_for_nodeport_boundary_and_health
|
||||
printf 'ROLLBACK complete. Forwarded-header trust removal was requested.\n' >&2
|
||||
set -e
|
||||
fi
|
||||
|
||||
case "$work_dir" in
|
||||
/tmp/platform-traefik-trust.*|"${TMPDIR:-/tmp}"/platform-traefik-trust.*)
|
||||
rm -rf -- "$work_dir"
|
||||
;;
|
||||
*)
|
||||
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
|
||||
"$work_dir" >&2
|
||||
;;
|
||||
esac
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
trust_render="${work_dir}/trust.yaml"
|
||||
render_overlay "$TRUST_OVERLAY" "$trust_render"
|
||||
|
||||
assert_live_baseline
|
||||
assert_runtime observe
|
||||
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
|
||||
"${work_dir}/live-observe.yaml"
|
||||
assert_nodeport_boundary_and_health
|
||||
|
||||
fresh_client_host="$(observe_host_nginx_client_host)"
|
||||
fresh_cidr="$(host_to_exact_cidr "$fresh_client_host")"
|
||||
[[ "$fresh_cidr" == "$reviewed_cidr" ]] || \
|
||||
fail "fresh ClientHost ${fresh_client_host} differs from reviewed ${REVIEWED_CLIENT_HOST}"
|
||||
|
||||
selected_context="$(current_context)"
|
||||
selected_api_server="$(current_api_server)"
|
||||
printf '\nKubernetes context: %s\nAPI server: %s\nTarget node: %s\n' \
|
||||
"$selected_context" "$selected_api_server" "$TARGET_NODE"
|
||||
printf 'Fresh ClientHost: %s\nExact trusted CIDR: %s\n' \
|
||||
"$fresh_client_host" "$declared_cidr"
|
||||
printf 'Type APPLY %s TRUST %s to continue: ' \
|
||||
"$selected_context" "$declared_cidr"
|
||||
read -r confirmation
|
||||
[[ "$confirmation" == "APPLY ${selected_context} TRUST ${declared_cidr}" ]] || \
|
||||
fail "cancelled"
|
||||
|
||||
[[ "$(current_context)" == "$selected_context" ]] || \
|
||||
fail "kubectl context changed after confirmation"
|
||||
[[ "$(current_api_server)" == "$selected_api_server" ]] || \
|
||||
fail "Kubernetes API server changed after confirmation"
|
||||
assert_live_baseline
|
||||
assert_runtime observe
|
||||
assert_nodeport_boundary_and_health
|
||||
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
|
||||
"${work_dir}/pre-apply-observe.yaml"
|
||||
|
||||
rollback_required=true
|
||||
kubectl apply --filename "$trust_render"
|
||||
|
||||
wait_for_runtime trust "$declared_cidr"
|
||||
kubectl --namespace kube-system rollout status deployment/traefik --timeout=5m
|
||||
assert_runtime trust "$declared_cidr"
|
||||
assert_live_hcc_matches_overlay "$TRUST_OVERLAY" \
|
||||
"${work_dir}/post-apply-trust.yaml"
|
||||
assert_live_baseline
|
||||
wait_for_nodeport_boundary_and_health
|
||||
assert_manifest_https
|
||||
|
||||
rollback_required=false
|
||||
printf '\nTRUST PHASE READY\n'
|
||||
printf 'Traefik web trusted CIDR: %s\n' "$declared_cidr"
|
||||
printf 'Traefik websecure trusted CIDR: absent\n'
|
||||
printf 'forwardedHeaders.insecure: absent\n'
|
||||
printf 'Gitea health and HTTPS site-manifest checks: PASS\n'
|
||||
printf 'Repeat the 30080/30443 refusal check from a separate LAN client.\n'
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 이 파일은 같은 디렉터리의 실행 스크립트에서만 source한다.
|
||||
|
||||
readonly TRAEFIK_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
readonly BASELINE_OVERLAY="${TRAEFIK_ROOT}/overlays/baseline"
|
||||
readonly OBSERVE_OVERLAY="${TRAEFIK_ROOT}/overlays/observe"
|
||||
readonly TRUST_OVERLAY="${TRAEFIK_ROOT}/overlays/trust"
|
||||
readonly TRUST_PATCH="${TRUST_OVERLAY}/trusted-proxy-cidr-patch.yaml"
|
||||
readonly SENTINEL_TRUSTED_PROXY_CIDR="192.0.2.1/32"
|
||||
|
||||
readonly TARGET_NODE="donghyeon-system-product-name"
|
||||
readonly K3S_NODEPORT_CONFIG="/etc/rancher/k3s/config.yaml.d/30-nodeport-loopback.yaml"
|
||||
readonly EXPECTED_K3S_CHART="https://%{KUBERNETES_API}%/static/charts/traefik-40.1.3+up40.1.0.tgz"
|
||||
readonly EXPECTED_CHART_LABEL="traefik-40.1.3_up40.1.0"
|
||||
readonly EXPECTED_TRAEFIK_IMAGE="rancher/mirrored-library-traefik:3.7.4"
|
||||
readonly GITEA_HOST="git.learn.hyeonworks.com"
|
||||
readonly GITEA_HEALTH_URL="http://127.0.0.1:30080/api/healthz"
|
||||
readonly GITEA_HTTPS_HEALTH_URL="https://${GITEA_HOST}/api/healthz"
|
||||
readonly GITEA_MANIFEST_URL="https://${GITEA_HOST}/assets/site-manifest.json"
|
||||
|
||||
fail() {
|
||||
printf 'ERROR: %s\n' "$*" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
require_commands() {
|
||||
local command_name
|
||||
|
||||
for command_name in "$@"; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || \
|
||||
fail "${command_name} is required"
|
||||
done
|
||||
}
|
||||
|
||||
render_overlay() {
|
||||
local overlay="$1"
|
||||
local output="$2"
|
||||
|
||||
kubectl kustomize "$overlay" >"$output"
|
||||
[[ -s "$output" ]] || fail "rendered manifest is empty: ${overlay}"
|
||||
}
|
||||
|
||||
manifest_values_content() {
|
||||
local manifest="$1"
|
||||
|
||||
awk '
|
||||
/^ valuesContent: \|-$/ {
|
||||
found = 1
|
||||
next
|
||||
}
|
||||
found {
|
||||
sub(/^ /, "")
|
||||
print
|
||||
}
|
||||
' "$manifest"
|
||||
}
|
||||
|
||||
source_trusted_proxy_cidr() {
|
||||
local -a values=()
|
||||
|
||||
mapfile -t values < <(
|
||||
awk -F'"' '/^[[:space:]]*-[[:space:]]*"/ { print $2 }' "$TRUST_PATCH"
|
||||
)
|
||||
[[ "${#values[@]}" -eq 1 ]] || \
|
||||
fail "trust patch must contain exactly one quoted trusted CIDR"
|
||||
printf '%s\n' "${values[0]}"
|
||||
}
|
||||
|
||||
host_to_exact_cidr() {
|
||||
local host="$1"
|
||||
|
||||
python3 - "$host" <<'PY'
|
||||
import ipaddress
|
||||
import sys
|
||||
|
||||
value = sys.argv[1]
|
||||
if "/" in value:
|
||||
raise SystemExit("ClientHost must be one IP address, not a CIDR")
|
||||
|
||||
address = ipaddress.ip_address(value)
|
||||
if address.is_unspecified or address.is_multicast:
|
||||
raise SystemExit("ClientHost cannot be unspecified or multicast")
|
||||
|
||||
prefix = 32 if address.version == 4 else 128
|
||||
print(f"{address.compressed}/{prefix}")
|
||||
PY
|
||||
}
|
||||
|
||||
normalize_exact_host_cidr() {
|
||||
local cidr="$1"
|
||||
|
||||
python3 - "$cidr" <<'PY'
|
||||
import ipaddress
|
||||
import sys
|
||||
|
||||
network = ipaddress.ip_network(sys.argv[1], strict=True)
|
||||
required_prefix = 32 if network.version == 4 else 128
|
||||
if network.prefixlen != required_prefix:
|
||||
raise SystemExit(
|
||||
f"trusted proxy range must be one exact host /{required_prefix}, "
|
||||
f"not {network.with_prefixlen}"
|
||||
)
|
||||
print(network.with_prefixlen)
|
||||
PY
|
||||
}
|
||||
|
||||
current_context() {
|
||||
kubectl config current-context
|
||||
}
|
||||
|
||||
current_api_server() {
|
||||
kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}'
|
||||
}
|
||||
|
||||
deployment_args() {
|
||||
kubectl --namespace kube-system get deployment traefik --output=json |
|
||||
jq --raw-output '
|
||||
.spec.template.spec.containers[]
|
||||
| select(.name == "traefik")
|
||||
| .args[]
|
||||
'
|
||||
}
|
||||
|
||||
runtime_matches() {
|
||||
local mode="$1"
|
||||
local trusted_cidr="${2:-}"
|
||||
local args
|
||||
|
||||
args="$(deployment_args 2>/dev/null)" || return 1
|
||||
if rg --quiet --ignore-case -- 'forwardedheaders\.insecure' <<<"$args"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$mode" != "baseline" ]]; then
|
||||
rg --quiet --fixed-strings --line-regexp -- '--accesslog=true' <<<"$args" || \
|
||||
return 1
|
||||
rg --quiet --fixed-strings --line-regexp -- '--accesslog.format=json' <<<"$args" || \
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$mode" in
|
||||
observe)
|
||||
! rg --quiet --ignore-case -- 'forwardedheaders\.trustedips' <<<"$args"
|
||||
;;
|
||||
trust)
|
||||
rg --quiet --fixed-strings --line-regexp -- \
|
||||
"--entryPoints.web.forwardedHeaders.trustedIPs=${trusted_cidr}" <<<"$args" || \
|
||||
return 1
|
||||
! rg --quiet --ignore-case -- \
|
||||
'entrypoints\.websecure\.forwardedheaders\.(trustedips|insecure)' <<<"$args"
|
||||
;;
|
||||
baseline)
|
||||
! rg --quiet --ignore-case -- \
|
||||
'accesslog|forwardedheaders\.(trustedips|insecure)' <<<"$args"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
assert_runtime() {
|
||||
local mode="$1"
|
||||
local trusted_cidr="${2:-}"
|
||||
local args
|
||||
local trusted_count
|
||||
|
||||
args="$(deployment_args)"
|
||||
rg --quiet --fixed-strings --line-regexp -- '--accesslog=true' <<<"$args" || \
|
||||
fail "Traefik runtime is missing --accesslog=true"
|
||||
rg --quiet --fixed-strings --line-regexp -- '--accesslog.format=json' <<<"$args" || \
|
||||
fail "Traefik runtime is missing JSON access-log format"
|
||||
if rg --quiet --ignore-case -- 'forwardedheaders\.insecure' <<<"$args"; then
|
||||
fail "forwardedHeaders.insecure must never be present"
|
||||
fi
|
||||
|
||||
trusted_count="$(
|
||||
rg --count --ignore-case -- 'forwardedheaders\.trustedips' <<<"$args" || true
|
||||
)"
|
||||
trusted_count="${trusted_count:-0}"
|
||||
|
||||
case "$mode" in
|
||||
observe)
|
||||
[[ "$trusted_count" == "0" ]] || \
|
||||
fail "observation phase must not trust forwarded headers"
|
||||
;;
|
||||
trust)
|
||||
[[ "$trusted_count" == "1" ]] || \
|
||||
fail "trust phase must render exactly one trustedIPs argument"
|
||||
rg --quiet --fixed-strings --line-regexp -- \
|
||||
"--entryPoints.web.forwardedHeaders.trustedIPs=${trusted_cidr}" <<<"$args" || \
|
||||
fail "web entrypoint does not contain the reviewed exact-host CIDR"
|
||||
if rg --quiet --ignore-case -- \
|
||||
'entrypoints\.websecure\.forwardedheaders\.(trustedips|insecure)' <<<"$args"; then
|
||||
fail "websecure must not receive forwarded-header trust"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
fail "unsupported runtime assertion mode: ${mode}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
wait_for_runtime() {
|
||||
local mode="$1"
|
||||
local trusted_cidr="${2:-}"
|
||||
local attempt
|
||||
|
||||
for ((attempt = 1; attempt <= 120; attempt++)); do
|
||||
if runtime_matches "$mode" "$trusted_cidr"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fail "Traefik runtime did not reach ${mode} state within 240 seconds"
|
||||
}
|
||||
|
||||
assert_live_baseline() {
|
||||
local chart
|
||||
local chart_label
|
||||
local image
|
||||
local nodeport_matches
|
||||
local service_json
|
||||
|
||||
kubectl get node "$TARGET_NODE" >/dev/null
|
||||
|
||||
chart="$(
|
||||
kubectl --namespace kube-system get helmchart.helm.cattle.io traefik \
|
||||
--output=jsonpath='{.spec.chart}'
|
||||
)"
|
||||
[[ "$chart" == "$EXPECTED_K3S_CHART" ]] || \
|
||||
fail "unexpected packaged Traefik chart: ${chart}"
|
||||
|
||||
chart_label="$(
|
||||
kubectl --namespace kube-system get deployment traefik \
|
||||
--output=jsonpath='{.metadata.labels.helm\.sh/chart}'
|
||||
)"
|
||||
[[ "$chart_label" == "$EXPECTED_CHART_LABEL" ]] || \
|
||||
fail "unexpected live Traefik chart label: ${chart_label}"
|
||||
|
||||
image="$(
|
||||
kubectl --namespace kube-system get deployment traefik --output=json |
|
||||
jq --raw-output '
|
||||
.spec.template.spec.containers[]
|
||||
| select(.name == "traefik")
|
||||
| .image
|
||||
'
|
||||
)"
|
||||
[[ "$image" == "$EXPECTED_TRAEFIK_IMAGE" ]] || \
|
||||
fail "unexpected live Traefik image: ${image}"
|
||||
|
||||
[[ -r "$K3S_NODEPORT_CONFIG" ]] || \
|
||||
fail "cannot read the k3s nodeport-addresses drop-in: ${K3S_NODEPORT_CONFIG}"
|
||||
rg --quiet --fixed-strings --line-regexp -- \
|
||||
' - "nodeport-addresses=127.0.0.0/8"' "$K3S_NODEPORT_CONFIG" || \
|
||||
fail "k3s nodeport-addresses is not pinned to 127.0.0.0/8"
|
||||
|
||||
nodeport_matches="$(
|
||||
rg --no-heading --line-number -- 'nodeport-addresses[=:]' \
|
||||
/etc/rancher/k3s/config.yaml \
|
||||
/etc/rancher/k3s/config.yaml.d 2>/dev/null || true
|
||||
)"
|
||||
[[ "$(wc -l <<<"$nodeport_matches" | tr -d '[:space:]')" == "1" ]] || \
|
||||
fail "nodeport-addresses must have exactly one k3s configuration owner"
|
||||
rg --quiet --fixed-strings -- "$K3S_NODEPORT_CONFIG" <<<"$nodeport_matches" || \
|
||||
fail "nodeport-addresses is owned by an unexpected k3s configuration file"
|
||||
|
||||
service_json="$(
|
||||
kubectl --namespace kube-system get service traefik --output=json
|
||||
)"
|
||||
jq --exit-status '
|
||||
.spec.type == "NodePort"
|
||||
and .spec.externalTrafficPolicy == "Cluster"
|
||||
and (.spec.ports | length) == 2
|
||||
and any(.spec.ports[];
|
||||
.name == "web"
|
||||
and .port == 80
|
||||
and .nodePort == 30080
|
||||
and .protocol == "TCP")
|
||||
and any(.spec.ports[];
|
||||
.name == "websecure"
|
||||
and .port == 443
|
||||
and .nodePort == 30443
|
||||
and .protocol == "TCP")
|
||||
' >/dev/null <<<"$service_json" || \
|
||||
fail "Traefik Service no longer matches the 80/30080 and 443/30443 boundary"
|
||||
}
|
||||
|
||||
assert_health_body() {
|
||||
local body="$1"
|
||||
|
||||
jq --exit-status '
|
||||
.status == "pass"
|
||||
and (.checks["database:ping"] | length) > 0
|
||||
and all(.checks["database:ping"][]; .status == "pass")
|
||||
and (.checks["cache:ping"] | length) > 0
|
||||
and all(.checks["cache:ping"][]; .status == "pass")
|
||||
' >/dev/null <<<"$body" || fail "Gitea database/cache health is not pass"
|
||||
}
|
||||
|
||||
assert_nodeport_boundary_and_health() {
|
||||
local body
|
||||
local node_ip
|
||||
local port
|
||||
|
||||
node_ip="$(
|
||||
kubectl get node "$TARGET_NODE" --output=json |
|
||||
jq --raw-output '
|
||||
[.status.addresses[] | select(.type == "InternalIP") | .address]
|
||||
| if length == 1 then .[0] else empty end
|
||||
'
|
||||
)"
|
||||
[[ -n "$node_ip" && "$node_ip" != "127.0.0.1" ]] || \
|
||||
fail "could not resolve exactly one non-loopback node InternalIP"
|
||||
|
||||
for port in 30080 30443; do
|
||||
nc -z -w 3 127.0.0.1 "$port" >/dev/null 2>&1 || \
|
||||
fail "loopback NodePort is not listening: 127.0.0.1:${port}"
|
||||
if nc -z -w 3 "$node_ip" "$port" >/dev/null 2>&1; then
|
||||
fail "NodePort escaped the loopback boundary: ${node_ip}:${port}"
|
||||
fi
|
||||
done
|
||||
|
||||
body="$(
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--header "Host: ${GITEA_HOST}" \
|
||||
"$GITEA_HEALTH_URL"
|
||||
)"
|
||||
assert_health_body "$body"
|
||||
|
||||
body="$(
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--resolve "${GITEA_HOST}:443:127.0.0.1" \
|
||||
"$GITEA_HTTPS_HEALTH_URL"
|
||||
)"
|
||||
assert_health_body "$body"
|
||||
}
|
||||
|
||||
wait_for_nodeport_boundary_and_health() {
|
||||
local attempt
|
||||
|
||||
for ((attempt = 1; attempt <= 60; attempt++)); do
|
||||
if assert_nodeport_boundary_and_health >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 마지막 검사는 오류 원인을 숨기지 않고 그대로 출력한다.
|
||||
assert_nodeport_boundary_and_health
|
||||
fail "Traefik NodePort boundary and Gitea health did not recover within 120 seconds"
|
||||
}
|
||||
|
||||
observe_host_nginx_client_host() {
|
||||
local http_code
|
||||
local logs
|
||||
local matched
|
||||
local nonce
|
||||
local observed
|
||||
local probe_path
|
||||
local router_count
|
||||
local since
|
||||
local -a client_hosts=()
|
||||
local attempt
|
||||
|
||||
since="$(date --utc '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
nonce="$(date --utc '+%Y%m%dT%H%M%S')-${BASHPID}"
|
||||
probe_path="/api/healthz/traefik-source-${nonce}"
|
||||
|
||||
http_code="$(
|
||||
curl --silent --show-error \
|
||||
--resolve "${GITEA_HOST}:443:127.0.0.1" \
|
||||
--output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
"https://${GITEA_HOST}${probe_path}"
|
||||
)"
|
||||
[[ "$http_code" == "404" ]] || \
|
||||
fail "unique Host Nginx observation request returned HTTP ${http_code}, expected 404"
|
||||
|
||||
matched=""
|
||||
for ((attempt = 1; attempt <= 20; attempt++)); do
|
||||
logs="$(
|
||||
kubectl --namespace kube-system logs deployment/traefik \
|
||||
--since-time "$since"
|
||||
)"
|
||||
matched="$(
|
||||
jq --raw-input --compact-output --arg path "$probe_path" '
|
||||
fromjson?
|
||||
| select(.RequestPath == $path)
|
||||
' <<<"$logs"
|
||||
)"
|
||||
[[ -n "$matched" ]] && break
|
||||
sleep 1
|
||||
done
|
||||
[[ -n "$matched" ]] || \
|
||||
fail "the unique request was not found in Traefik JSON access logs"
|
||||
|
||||
router_count="$(
|
||||
jq --slurp '
|
||||
[
|
||||
.[]
|
||||
| select(
|
||||
((.RouterName // "") | ascii_downcase | contains("gitea"))
|
||||
)
|
||||
]
|
||||
| length
|
||||
' <<<"$matched"
|
||||
)"
|
||||
[[ "$router_count" -ge 1 ]] || \
|
||||
fail "the observation log did not traverse a Gitea router"
|
||||
|
||||
mapfile -t client_hosts < <(
|
||||
jq --raw-output '
|
||||
select((.RouterName // "") | ascii_downcase | contains("gitea"))
|
||||
| .ClientHost // empty
|
||||
' <<<"$matched" |
|
||||
sort --unique
|
||||
)
|
||||
[[ "${#client_hosts[@]}" -eq 1 && -n "${client_hosts[0]}" ]] || \
|
||||
fail "expected one distinct Traefik ClientHost for the unique request"
|
||||
|
||||
observed="${client_hosts[0]}"
|
||||
host_to_exact_cidr "$observed" >/dev/null
|
||||
printf '%s\n' "$observed"
|
||||
}
|
||||
|
||||
assert_manifest_https() {
|
||||
local body
|
||||
local attempt
|
||||
|
||||
for ((attempt = 1; attempt <= 30; attempt++)); do
|
||||
body="$(
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--resolve "${GITEA_HOST}:443:127.0.0.1" \
|
||||
"$GITEA_MANIFEST_URL"
|
||||
)"
|
||||
if jq --exit-status --arg expected "https://${GITEA_HOST}/" '
|
||||
.start_url == $expected
|
||||
and all(.icons[]; (.src | startswith("https://")))
|
||||
' >/dev/null <<<"$body"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fail "Gitea site manifest did not stabilize on HTTPS URLs"
|
||||
}
|
||||
|
||||
assert_live_hcc_matches_overlay() {
|
||||
local overlay="$1"
|
||||
local render_file="$2"
|
||||
local actual
|
||||
local expected
|
||||
|
||||
render_overlay "$overlay" "$render_file"
|
||||
expected="$(manifest_values_content "$render_file")"
|
||||
actual="$(
|
||||
kubectl --namespace kube-system \
|
||||
get helmchartconfig.helm.cattle.io traefik --output=json |
|
||||
jq --raw-output '.spec.valuesContent'
|
||||
)"
|
||||
[[ "$actual" == "$expected" ]] || \
|
||||
fail "live Traefik HelmChartConfig does not match the expected overlay"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
set +x
|
||||
umask 077
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck source=common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: bash infrastructure/networking/traefik/scripts/observe-client-host.sh
|
||||
|
||||
Sends one unique HTTPS request through Host Nginx and extracts the corresponding
|
||||
ClientHost from Traefik JSON access logs. It performs no cluster mutation.
|
||||
USAGE
|
||||
}
|
||||
|
||||
[[ "$#" -eq 0 ]] || {
|
||||
usage
|
||||
exit 2
|
||||
}
|
||||
|
||||
require_commands kubectl jq rg curl nc python3 awk sort date
|
||||
|
||||
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-clienthost.XXXXXX")"
|
||||
cleanup() {
|
||||
case "$work_dir" in
|
||||
/tmp/platform-traefik-clienthost.*|"${TMPDIR:-/tmp}"/platform-traefik-clienthost.*)
|
||||
rm -rf -- "$work_dir"
|
||||
;;
|
||||
*)
|
||||
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
|
||||
"$work_dir" >&2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
bash "${SCRIPT_DIR}/validate.sh"
|
||||
assert_live_baseline
|
||||
assert_runtime observe
|
||||
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
|
||||
"${work_dir}/live-observe.yaml"
|
||||
assert_nodeport_boundary_and_health
|
||||
|
||||
observed_client_host="$(observe_host_nginx_client_host)"
|
||||
trusted_proxy_cidr="$(host_to_exact_cidr "$observed_client_host")"
|
||||
|
||||
printf '\nTRAEFIK SOURCE OBSERVED\n'
|
||||
printf 'ClientHost: %s\n' "$observed_client_host"
|
||||
printf 'Minimum trusted CIDR: %s\n' "$trusted_proxy_cidr"
|
||||
printf 'Record that CIDR in:\n%s\n' "$TRUST_PATCH"
|
||||
printf 'Replace only the sentinel %s, then run apply-trust.sh with this ClientHost.\n' \
|
||||
"$SENTINEL_TRUSTED_PROXY_CIDR"
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
set +x
|
||||
umask 077
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck source=common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
bash infrastructure/networking/traefik/scripts/rollback-to-observe.sh --execute
|
||||
|
||||
Removes forwarded-header trust while retaining JSON access logging.
|
||||
USAGE
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "--execute" && "$#" -eq 1 ]] || {
|
||||
usage
|
||||
exit 2
|
||||
}
|
||||
|
||||
require_commands kubectl jq rg curl nc python3 awk cmp find bash
|
||||
|
||||
bash "${SCRIPT_DIR}/validate.sh"
|
||||
assert_live_baseline
|
||||
kubectl --namespace kube-system \
|
||||
get helmchartconfig.helm.cattle.io traefik >/dev/null
|
||||
assert_nodeport_boundary_and_health
|
||||
|
||||
selected_context="$(current_context)"
|
||||
selected_api_server="$(current_api_server)"
|
||||
printf '\nKubernetes context: %s\nAPI server: %s\nTarget node: %s\n' \
|
||||
"$selected_context" "$selected_api_server" "$TARGET_NODE"
|
||||
printf 'Type ROLLBACK %s OBSERVE to remove forwarded-header trust: ' \
|
||||
"$selected_context"
|
||||
read -r confirmation
|
||||
[[ "$confirmation" == "ROLLBACK ${selected_context} OBSERVE" ]] || \
|
||||
fail "cancelled"
|
||||
|
||||
[[ "$(current_context)" == "$selected_context" ]] || \
|
||||
fail "kubectl context changed after confirmation"
|
||||
[[ "$(current_api_server)" == "$selected_api_server" ]] || \
|
||||
fail "Kubernetes API server changed after confirmation"
|
||||
assert_live_baseline
|
||||
|
||||
kubectl apply --kustomize "$OBSERVE_OVERLAY"
|
||||
wait_for_runtime observe
|
||||
kubectl --namespace kube-system rollout status deployment/traefik --timeout=5m
|
||||
assert_runtime observe
|
||||
|
||||
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-rollback.XXXXXX")"
|
||||
cleanup() {
|
||||
case "$work_dir" in
|
||||
/tmp/platform-traefik-rollback.*|"${TMPDIR:-/tmp}"/platform-traefik-rollback.*)
|
||||
rm -rf -- "$work_dir"
|
||||
;;
|
||||
*)
|
||||
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
|
||||
"$work_dir" >&2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
assert_live_hcc_matches_overlay "$OBSERVE_OVERLAY" \
|
||||
"${work_dir}/post-rollback-observe.yaml"
|
||||
assert_live_baseline
|
||||
wait_for_nodeport_boundary_and_health
|
||||
|
||||
printf '\nROLLBACK COMPLETE\n'
|
||||
printf 'JSON access logging remains enabled.\n'
|
||||
printf 'forwardedHeaders trust is absent from both entrypoints.\n'
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
set +x
|
||||
umask 077
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck source=common.sh
|
||||
source "${SCRIPT_DIR}/common.sh"
|
||||
|
||||
assert_count() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local expected="$3"
|
||||
local description="$4"
|
||||
local actual
|
||||
|
||||
actual="$(rg --count --no-filename -- "$pattern" "$file" || true)"
|
||||
actual="${actual:-0}"
|
||||
[[ "$actual" == "$expected" ]] || \
|
||||
fail "${description}: expected ${expected}, found ${actual}"
|
||||
}
|
||||
|
||||
require_commands kubectl rg awk cmp python3 find bash
|
||||
|
||||
render_dir="$(mktemp -d "${TMPDIR:-/tmp}/platform-traefik-render.XXXXXX")"
|
||||
cleanup() {
|
||||
case "$render_dir" in
|
||||
/tmp/platform-traefik-render.*|"${TMPDIR:-/tmp}"/platform-traefik-render.*)
|
||||
rm -rf -- "$render_dir"
|
||||
;;
|
||||
*)
|
||||
printf 'WARNING: refusing to remove unexpected directory: %s\n' \
|
||||
"$render_dir" >&2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
root_render="${render_dir}/root.yaml"
|
||||
baseline_render="${render_dir}/baseline.yaml"
|
||||
observe_render="${render_dir}/observe.yaml"
|
||||
trust_render="${render_dir}/trust.yaml"
|
||||
|
||||
render_overlay "$TRAEFIK_ROOT" "$root_render"
|
||||
render_overlay "$BASELINE_OVERLAY" "$baseline_render"
|
||||
render_overlay "$OBSERVE_OVERLAY" "$observe_render"
|
||||
render_overlay "$TRUST_OVERLAY" "$trust_render"
|
||||
|
||||
cmp --silent "$root_render" "$observe_render" || \
|
||||
fail "the Traefik root must render the observation phase"
|
||||
|
||||
for manifest in "$baseline_render" "$observe_render" "$trust_render"; do
|
||||
assert_count "$manifest" '^apiVersion: helm\.cattle\.io/v1$' 1 \
|
||||
"HelmChartConfig API version"
|
||||
assert_count "$manifest" '^kind: HelmChartConfig$' 1 \
|
||||
"HelmChartConfig kind"
|
||||
assert_count "$manifest" '^ name: traefik$' 1 \
|
||||
"HelmChartConfig name"
|
||||
assert_count "$manifest" '^ namespace: kube-system$' 1 \
|
||||
"HelmChartConfig namespace"
|
||||
assert_count "$manifest" '^ failurePolicy: abort$' 1 \
|
||||
"Helm failure policy must preserve the running release on upgrade failure"
|
||||
assert_count "$manifest" '^ deployment:$' 1 \
|
||||
"Traefik Deployment values root"
|
||||
assert_count "$manifest" '^ podAnnotations:$' 1 \
|
||||
"Traefik Pod annotations block"
|
||||
assert_count "$manifest" '^ prometheus\.io/path: /metrics$' 1 \
|
||||
"Traefik legacy scrape path preservation"
|
||||
assert_count "$manifest" '^ prometheus\.io/port: "9100"$' 1 \
|
||||
"Traefik legacy scrape port preservation"
|
||||
assert_count "$manifest" '^ prometheus\.io/scrape: "true"$' 1 \
|
||||
"Traefik legacy scrape enablement preservation"
|
||||
assert_count "$manifest" '[Ii]nsecure' 0 \
|
||||
"insecure forwarded-header mode"
|
||||
assert_count "$manifest" '^ service:$' 1 \
|
||||
"Traefik Service values root"
|
||||
assert_count "$manifest" '^ type: NodePort$' 1 \
|
||||
"loopback NodePort Service type preservation"
|
||||
assert_count "$manifest" '^ externalTrafficPolicy: Cluster$' 1 \
|
||||
"Traefik externalTrafficPolicy preservation"
|
||||
assert_count "$manifest" '^ nodePort: 30080$' 1 \
|
||||
"Traefik web NodePort preservation"
|
||||
assert_count "$manifest" '^ nodePort: 30443$' 1 \
|
||||
"Traefik websecure NodePort preservation"
|
||||
assert_count "$manifest" '^ metrics:$' 1 \
|
||||
"Traefik metrics values root"
|
||||
assert_count "$manifest" '^ prometheus:$' 1 \
|
||||
"Traefik Prometheus metrics block"
|
||||
assert_count "$manifest" '^ serviceMonitor:$' 1 \
|
||||
"Traefik ServiceMonitor block"
|
||||
assert_count "$manifest" '^ jobLabel: app\.kubernetes\.io/name$' 1 \
|
||||
"Traefik ServiceMonitor job label"
|
||||
assert_count "$manifest" '^ observability\.hyeonworks\.com/instance: home$' 1 \
|
||||
"Traefik ServiceMonitor selector label"
|
||||
assert_count "$manifest" '^ interval: 30s$' 1 \
|
||||
"Traefik ServiceMonitor interval"
|
||||
assert_count "$manifest" '^ scrapeTimeout: 10s$' 1 \
|
||||
"Traefik ServiceMonitor timeout"
|
||||
assert_count "$manifest" '^ enabled: true$' 2 \
|
||||
"Traefik metrics Service and ServiceMonitor enablement"
|
||||
assert_count "$manifest" '^[[:space:]]*(type|serviceType):[[:space:]]*LoadBalancer' 0 \
|
||||
"LoadBalancer exposure"
|
||||
done
|
||||
|
||||
assert_count "$baseline_render" '^[[:space:]]*logs:' 0 \
|
||||
"baseline access logs"
|
||||
assert_count "$baseline_render" '^[[:space:]]*forwardedHeaders:' 0 \
|
||||
"baseline forwarded-header trust"
|
||||
|
||||
for manifest in "$observe_render" "$trust_render"; do
|
||||
assert_count "$manifest" '^ enabled: true$' 1 \
|
||||
"access log enablement"
|
||||
assert_count "$manifest" '^ format: json$' 1 \
|
||||
"JSON access log format"
|
||||
assert_count "$manifest" '^ defaultmode: keep$' 1 \
|
||||
"access-log general-field policy"
|
||||
assert_count "$manifest" '^ defaultmode: drop$' 1 \
|
||||
"access-log header policy"
|
||||
done
|
||||
|
||||
assert_count "$observe_render" '^[[:space:]]*forwardedHeaders:' 0 \
|
||||
"observation-phase forwarded-header trust"
|
||||
assert_count "$observe_render" '^[[:space:]]*trustedIPs:' 0 \
|
||||
"observation-phase trusted IP list"
|
||||
|
||||
assert_count "$trust_render" '^ ports:$' 1 \
|
||||
"trust-phase ports values root"
|
||||
assert_count "$trust_render" '^ web:$' 1 \
|
||||
"trust-phase web entrypoint"
|
||||
assert_count "$trust_render" '^ forwardedHeaders:$' 1 \
|
||||
"trust-phase forwarded-header block"
|
||||
assert_count "$trust_render" '^ trustedIPs:$' 1 \
|
||||
"trust-phase trusted IP list"
|
||||
|
||||
trusted_cidr="$(source_trusted_proxy_cidr)"
|
||||
normalized_cidr="$(normalize_exact_host_cidr "$trusted_cidr")"
|
||||
[[ "$trusted_cidr" == "$normalized_cidr" ]] || \
|
||||
fail "trusted proxy CIDR must use canonical exact-host notation"
|
||||
assert_count "$trust_render" \
|
||||
"^[[:space:]]*-[[:space:]]*\"${trusted_cidr//./\\.}\"[[:space:]]*$" 1 \
|
||||
"rendered exact-host trusted proxy CIDR"
|
||||
|
||||
while IFS= read -r -d '' script_path; do
|
||||
bash -n "$script_path"
|
||||
done < <(
|
||||
find "$SCRIPT_DIR" -maxdepth 1 -type f -name '*.sh' -print0
|
||||
)
|
||||
|
||||
printf 'Traefik observation and trust overlays rendered successfully.\n'
|
||||
printf 'Access logs are JSON and request headers are dropped.\n'
|
||||
printf 'No insecure mode, websecure trust, or LoadBalancer exposure was found.\n'
|
||||
printf 'The existing NodePort 30080/30443 boundary is declared in both phases.\n'
|
||||
if [[ "$trusted_cidr" == "$SENTINEL_TRUSTED_PROXY_CIDR" ]]; then
|
||||
printf 'Trust overlay remains intentionally blocked by sentinel CIDR %s.\n' \
|
||||
"$SENTINEL_TRUSTED_PROXY_CIDR"
|
||||
else
|
||||
printf 'Trust overlay contains reviewed exact-host CIDR %s.\n' "$trusted_cidr"
|
||||
fi
|
||||
Reference in New Issue
Block a user