How-toKubernetes

Gateway API ExternalAuth로 외부 OTLP 수집 보호하기

외부 OpenTelemetry 클라이언트의 로그, 메트릭과 트레이스를 하나의 HTTPS endpoint로 받고 Bearer token을 검증한 뒤 Collector로 전달합니다.

업데이트 검증 근거 자료이 페이지 편집

핵심 요약

  • 공개 OTLP endpoint는 Kubernetes Gateway의 443만 노출하고 Collector의 4317, 4318은 클러스터 내부에 유지함
  • HTTPRouteExternalAuth filter가 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가 HTTPRouteExternalAuth filter를 지원해야 함
    • ExternalAuth는 controller 지원 범위에 영향을 받으므로 다른 Gateway 구현으로 이동할 때 호환성을 다시 검증해야 함
    • Gateway listener는 HTTP/2와 TLS를 지원해야 OTLP/gRPC 요청을 같은 443 endpoint에서 받을 수 있음
  • OTel Collector는 클러스터 내부에서 OTLP/gRPC 4317과 OTLP/HTTP 4318을 수신해야 함
    • 외부 클라이언트가 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으로 전달함
otel-external-auth-secret.yaml
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을 반환함
Caddyfile
{
  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로 분리함
otel-external-route.yaml
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: 4318
  • Exact path match는 OTLP 이외의 임의 경로가 Collector에 도달하지 않도록 제한함
  • allowedHeaders에는 인증에 필요한 Authorization만 전달함
  • NetworkPolicy는 ingress data plane에서 인증 Service 8080과 Collector 4317, 4318로 필요한 흐름만 허용해야 함

Codex credential을 mise로 주입함

  • mise는 SOPS token을 읽는 helper를 실행하고 결과를 redacted 환경 변수로 제공함
mise.toml
[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만 선언함
~/.codex/config.toml
[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.0headers = { Authorization = "${TOKEN}" } 값을 치환하지 않고 문자열 그대로 전송함
    • 표준 OpenTelemetry 환경 변수 OTEL_EXPORTER_OTLP_HEADERS를 사용해야 함
    • 환경 변수와 설정은 process 시작 시 읽으므로 이미 실행 중인 Codex를 재시작해야 함
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/logs
  • mise가 제공한 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 경로에서 401200을 검증한 뒤 metrics, traces와 gRPC 경로를 확장함
  • 운영 반영은 DNS/TLS → Secret → auth Service → HTTPRoute → NetworkPolicy → client 순서로 진행함
  • 장애 대응 문서에는 token 회전, Route condition, Collector queue와 최종 저장소 조회 절차를 함께 포함해야 함

참고 문서