How-toKubernetes
Gateway API ExternalAuth로 외부 OTLP 수집 보호하기
외부 OpenTelemetry 클라이언트의 로그, 메트릭과 트레이스를 하나의 HTTPS endpoint로 받고 Bearer token을 검증한 뒤 Collector로 전달합니다.
핵심 요약
- 공개 OTLP endpoint는 Kubernetes Gateway의
443만 노출하고 Collector의4317,4318은 클러스터 내부에 유지함 HTTPRoute의ExternalAuthfilter가Authorization: Bearer <token>을 먼저 검증하고 성공한 요청만 Collector로 전달함- 토큰 원문은 Git에 저장하지 않고 SOPS ciphertext와 Kubernetes
Secret으로 관리함 - OTLP/gRPC method path는 내부
4317, OTLP/HTTP의/v1/logs,/v1/metrics,/v1/traces는 내부4318로 분기함 - Codex는
mise가 제공하는 표준OTEL_EXPORTER_OTLP_HEADERS환경 변수를 사용하고config.toml에는 endpoint만 유지함
적용 조건을 확인함
- Gateway controller가
HTTPRoute의ExternalAuthfilter를 지원해야 함ExternalAuth는 controller 지원 범위에 영향을 받으므로 다른 Gateway 구현으로 이동할 때 호환성을 다시 검증해야 함- Gateway listener는 HTTP/2와 TLS를 지원해야 OTLP/gRPC 요청을 같은
443endpoint에서 받을 수 있음
- OTel Collector는 클러스터 내부에서 OTLP/gRPC
4317과 OTLP/HTTP4318을 수신해야 함- 외부 클라이언트가 Collector Service나 NodePort에 직접 접근할 수 없어야 함
- DNS와 TLS 인증서가 공개 OTLP hostname을 포함해야 함
- 이 문서의 검증된 예시는
otel.jamie.kr을 사용함
- 이 문서의 검증된 예시는
요청 경로를 두 개의 신뢰 경계로 분리함
Codex 또는 외부 SDK
│ HTTPS :443 + Authorization
▼
Gateway / HTTPRoute
├─ ExternalAuth ──► auth Service :8080 ──► 200 또는 401
└─ 인증 성공
├─ gRPC method path ──► otel-gateway :4317
└─ /v1/{logs,metrics,traces} ──► otel-gateway :4318- Gateway가 인터넷 노출, TLS 종료, hostname과 허용 path를 소유함
- 인증 Service는 token 비교만 수행하고 telemetry payload를 저장하거나 전달하지 않음
- Collector는 인증이 끝난 요청의 decode, 처리와 exporter queue를 소유함
- 이 분리는 Collector 설정에 공개 인증 책임을 결합하지 않고 여러 OTLP signal에 동일한 정책을 적용함
Bearer token을 암호화된 desired state로 관리함
- 256-bit 무작위 token을 생성하고 shell history나 command argument에 원문을 넣지 않음
openssl rand -hex 32 | \
sops set --value-stdin \
apps/observability/clickstack/secrets.enc.yaml \
'["hyperdx"]["secrets"]["OTLP_INGEST_TOKEN"]'- Helm 또는 GitOps rendering 단계가 SOPS 값을 Kubernetes
Secret으로 전달함
apiVersion: v1
kind: Secret
metadata:
name: otel-external-auth-token
namespace: observability
type: Opaque
stringData:
token: "{{ .Values.hyperdx.secrets.OTLP_INGEST_TOKEN }}"- Git에는 SOPS ciphertext만 남아야 함
git diff와 secret scanner로 token 원문이 포함되지 않았는지 확인함- Kubernetes Secret은 암호화된 Git 저장 형식이 아니라 배포 시점의 런타임 자격 증명임
최소 인증 Service를 구성함
- 인증 backend는 정확한 Bearer token이면
200, 그 외 요청이면401을 반환함
{
admin off
auto_https off
}
:8080 {
route {
@health path /healthz
respond @health "ok" 200
@authorized header Authorization "Bearer {$OTLP_BEARER_TOKEN}"
header @authorized X-OTLP-Authenticated "true"
respond @authorized 200
respond 401
}
}- Deployment는 token을
Secret에서 환경 변수로 읽고 Service는ClusterIP:8080만 제공함automountServiceAccountToken: false를 사용함- non-root, read-only root filesystem과 drop-all capabilities를 적용함
- readiness와 liveness는 인증이 필요 없는
/healthz를 사용함 - 두 replica와
PodDisruptionBudget으로 단일 Pod 교체가 인증 경로 전체 장애가 되지 않게 함
OTLP signal만 정확한 path로 허용함
- 하나의
HTTPRoute에서 gRPC method path와 OTLP/HTTP path를 별도 rule로 분리함
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: otel-external
namespace: observability
spec:
parentRefs:
- name: public
namespace: gateway-system
sectionName: apps-https
hostnames: [otel.jamie.kr]
rules:
- matches:
- path:
type: Exact
value: /opentelemetry.proto.collector.logs.v1.LogsService/Export
- path:
type: Exact
value: /opentelemetry.proto.collector.metrics.v1.MetricsService/Export
- path:
type: Exact
value: /opentelemetry.proto.collector.trace.v1.TraceService/Export
filters:
- type: ExternalAuth
externalAuth:
protocol: HTTP
backendRef:
name: otel-external-auth
port: 8080
http:
path: /authorize
allowedHeaders: [Authorization]
allowedResponseHeaders: [X-OTLP-Authenticated]
backendRefs:
- name: otel-gateway
port: 4317
- matches:
- path: { type: Exact, value: /v1/logs }
- path: { type: Exact, value: /v1/metrics }
- path: { type: Exact, value: /v1/traces }
filters:
- type: ExternalAuth
externalAuth:
protocol: HTTP
backendRef:
name: otel-external-auth
port: 8080
http:
path: /authorize
allowedHeaders: [Authorization]
allowedResponseHeaders: [X-OTLP-Authenticated]
backendRefs:
- name: otel-gateway
port: 4318Exactpath match는 OTLP 이외의 임의 경로가 Collector에 도달하지 않도록 제한함allowedHeaders에는 인증에 필요한Authorization만 전달함- NetworkPolicy는 ingress data plane에서 인증 Service
8080과 Collector4317,4318로 필요한 흐름만 허용해야 함
Codex credential을 mise로 주입함
mise는 SOPS token을 읽는 helper를 실행하고 결과를 redacted 환경 변수로 제공함
[env]
OTEL_EXPORTER_OTLP_HEADERS = { value = "{{ exec(command='/secure/bin/otlp-authorization') | trim }}", redact = true }- helper는 token 원문이 아니라 OpenTelemetry header 환경 변수 형식을 출력함
Authorization=Bearer%20<64-hex-token>- Codex 설정에는 signal별 HTTPS endpoint만 선언함
[otel]
environment = "production"
log_user_prompt = false
exporter = { otlp-http = { endpoint = "https://otel.jamie.kr/v1/logs", protocol = "binary" } }
metrics_exporter = { otlp-http = { endpoint = "https://otel.jamie.kr/v1/metrics", protocol = "binary" } }
trace_exporter = { otlp-http = { endpoint = "https://otel.jamie.kr/v1/traces", protocol = "binary" } }- 검증일 기준 Codex
0.151.0은headers = { Authorization = "${TOKEN}" }값을 치환하지 않고 문자열 그대로 전송함- 표준 OpenTelemetry 환경 변수
OTEL_EXPORTER_OTLP_HEADERS를 사용해야 함 - 환경 변수와 설정은 process 시작 시 읽으므로 이미 실행 중인 Codex를 재시작해야 함
- 표준 OpenTelemetry 환경 변수
mise exec -- codex거부와 허용 경로를 함께 검증함
- token이 없는 요청은 Gateway에서
401이어야 함
curl --silent --output /dev/null --write-out '%{http_code}\n' \
--request POST \
--header 'Content-Type: application/x-protobuf' \
--data-binary '' \
https://otel.jamie.kr/v1/logsmise가 제공한 token 요청은 Collector의200응답까지 도달해야 함
mise exec -- bash -c '
authorization="${OTEL_EXPORTER_OTLP_HEADERS#Authorization=}"
authorization="${authorization//%20/ }"
curl --silent --output /dev/null --write-out "%{http_code}\n" \
--request POST \
--header "Content-Type: application/x-protobuf" \
--header "Authorization: ${authorization}" \
--data-binary "" \
https://otel.jamie.kr/v1/logs
unset authorization
'- Kubernetes 상태는 Route condition과 backend rollout을 함께 확인함
kubectl -n observability get httproute otel-external -o wide
kubectl -n observability rollout status deployment/otel-external-auth
kubectl -n observability rollout status statefulset/otel-gateway- 실제 완료 조건은 인증 성공뿐 아니라 logs, metrics와 traces가 저장 backend에서 조회되는 상태임
- 짧은 Codex 시작 검증은 log만 생성할 수 있으므로 실제 turn 이후 세 signal을 각각 확인함
token 회전과 한계를 관리함
- 새 token을 SOPS에 반영하고 auth Deployment가 새 Secret으로 재시작된 뒤 클라이언트를 갱신함
- 단일 token 비교 방식은 이전 token과 새 token의 무중단 중첩 기간을 제공하지 않음
- 무중단 회전이 필요하면 인증 Service가 두 token을 한시적으로 허용하도록 별도 계약이 필요함
- Bearer token은 클라이언트 식별, 세분화된 권한과 사용량 제한을 제공하지 않음
- 클라이언트별 폐기와 감사가 필요하면 token hash registry, mTLS 또는 OIDC 기반 인증을 검토함
- 인증은 과도한 telemetry 전송을 막지 않음
- Gateway 또는 Collector에 request size, rate limit, queue와 memory limit을 별도로 적용해야 함
실행 제안
- 먼저 OTLP/HTTP logs 경로에서
401과200을 검증한 뒤 metrics, traces와 gRPC 경로를 확장함 - 운영 반영은
DNS/TLS → Secret → auth Service → HTTPRoute → NetworkPolicy → client순서로 진행함 - 장애 대응 문서에는 token 회전, Route condition, Collector queue와 최종 저장소 조회 절차를 함께 포함해야 함