2026-03-11 | Swagger 403, ArgoCD 대시보드, CORS 이슈 해결


1. Swagger 403 → OAuth 로그인 리다이렉트#

문제#

  • swagger.dev.goormgb.space 접속 시 403 Forbidden 페이지 표시
  • 로그인 페이지로 리다이렉트 되지 않음

원인#

  • 브라우저에 stale cookie (만료된 _oauth2_proxy 쿠키) 존재
  • OAuth2 Proxy가 쿠키를 검증하고 “유효하지 않음” → 403 반환
  • 정상 흐름: 쿠키 없음 → 302 리다이렉트 → 로그인
[정상 흐름 - 쿠키 없음]
사용자 → OAuth2 Proxy → 쿠키 없음 → 302 → Google 로그인

[문제 상황 - 만료된 쿠키]
사용자 → OAuth2 Proxy → 쿠키 있음 (만료됨) → 403 Forbidden
                                              ↑ 여기서 멈춤!

해결: EnvoyFilter로 403→302 리다이렉트#

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: swagger-403-redirect
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: GATEWAY
        listener:
          filterChain:
            filter:
              name: envoy.filters.network.http_connection_manager
              subFilter:
                name: envoy.filters.http.router
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.lua
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
            inlineCode: |
              function envoy_on_response(response_handle)
                local host = response_handle:headers():get(":authority") or ""
                local status = response_handle:headers():get(":status")

                -- swagger 도메인의 403 응답만 처리
                if string.find(host, "swagger") and status == "403" then
                  local path = response_handle:headers():get("x-original-uri") or "/"
                  local redirect_url = "/oauth2/start?rd=" .. path

                  -- 302 리다이렉트로 변경
                  response_handle:headers():replace(":status", "302")
                  response_handle:headers():add("location", redirect_url)
                  response_handle:headers():add("cache-control", "no-cache, no-store")
                end
              end

핵심 포인트#

  • envoy_on_response: 응답 단계에서 가로채기
  • swagger 도메인만 처리 (다른 서비스 영향 없음)
  • 403 → 302 변환 + /oauth2/start 리다이렉트
  • OAuth2 Proxy가 다시 로그인 프로세스 시작

2. CORS 에러로 프론트엔드 “하얗게 터짐”#

문제#

  • 프론트엔드에서 API 호출 시 CORS 에러
  • 브라우저 콘솔에 에러만 보이고 페이지 하얗게 터짐

원인#

  • 백엔드가 400, 500 에러 응답에 CORS 헤더를 포함하지 않음
[정상 응답 200]
Access-Control-Allow-Origin: https://dev.goormgb.space  ✓
→ 프론트엔드가 응답 본문 읽기 가능

[에러 응답 500]
Access-Control-Allow-Origin: (없음)  ✗
→ 브라우저가 응답 차단 → 프론트엔드에서 에러 원인 파악 불가
→ 에러 핸들링 불가 → 하얗게 터짐

왜 에러 응답에도 CORS 헤더가 필요한가?#

  1. 브라우저 보안 정책: Origin이 다르면 모든 응답에 CORS 헤더 필요
  2. 에러 핸들링: CORS 헤더 없으면 response.json() 자체가 불가능
  3. UX 개선: 에러 메시지를 사용자에게 보여줄 수 있음

해결 방안 (백엔드 팀 전달)#

// Spring Boot - 에러 응답에도 CORS 헤더 포함
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("https://dev.goormgb.space")
            .allowedMethods("*")
            .allowCredentials(true);
    }
}

// 또는 @ControllerAdvice에서 모든 예외 응답에 헤더 추가

3. /auth/token/refresh 404 에러#

문제#

  • 프론트엔드에서 /auth/token/refresh 호출 시 404

원인: 경로 불일치#

구분경로
프론트엔드 호출/auth/token/refresh
API Gateway 라우팅Path=/auth/** → Auth-Guard
Auth-Guard 실제 경로/token/refresh (클래스 레벨 @RequestMapping 없음)
프론트: /auth/token/refresh
           ↓
API Gateway: /auth/** 매칭 → Auth-Guard로 전달
           ↓
Auth-Guard: /auth/token/refresh 받음
           ↓
404! (실제로는 /token/refresh만 존재)

해결 방안 (백엔드 팀 전달)#

옵션 1: StripPrefix 필터 추가 (권장)

routes:
  - id: auth-guard
    uri: ${AUTH_GUARD_URL}
    predicates:
      - Path=/auth/**
    filters:
      - StripPrefix=1  # /auth/token/refresh → /token/refresh

옵션 2: Controller에 @RequestMapping 추가

@RestController
@RequestMapping("/auth")  // 추가
public class AuthController {

4. ArgoCD 대시보드 개선#

변경사항#

  1. 대시보드 분리

    • argocd-app-deployments.json - 서비스 앱 (dev-*)
    • argocd-infra-deployments.json - 인프라 (Istio, Monitoring 등)
  2. Unhealthy 쿼리 수정

    • 기존: health_status=~"Degraded|Progressing|Unknown" (Progressing도 Unhealthy로 카운트)
    • 변경: health_status="Degraded" + OR on() vector(0) (결과 없을 때 0 표시)
  3. 로그 섹션 추가 (두 대시보드 동일)

    • Reconciliation 건수 (Loki timeseries)
    • 에러 로그 (Loki logs)
    • Webhook 수신 로그
    • Sync 완료 로그
  4. 기본 시간 범위 변경

    • now-24hnow-30m
    • Loki 쿼리 타임아웃 문제 해결
  5. 헤더 한 줄로 변경 (스크롤바 제거)


파일 변경 목록#

생성#

  • dev/root/infra/security/swagger-403-redirect.yaml

수정#

  • common-charts/.../argocd-app-deployments.json
  • common-charts/.../argocd-infra-deployments.json
  • dev/root/kustomization.yaml (swagger-403-redirect 등록)

5. Helm Chart Dependency 관리#

문제#

Error: An error occurred while checking for chart dependencies.
You may need to run `helm dependency build` to fetch missing dependencies:
found in Chart.yaml, but missing in charts/ directory: base

ArgoCD가 helm template 실행 시 dependency를 찾지 못함.

원인#

  • Helm chart의 Chart.yaml에 dependencies 정의됨
  • helm dependency build 실행 시 charts/ 폴더에 다운로드됨
  • .gitignorecharts/가 포함되어 있거나, 커밋되지 않은 경우 ArgoCD가 사용 불가

해결: 주기적 dependency 업데이트#

# 1. Helm repo 추가 (최초 1회)
helm repo add istio https://istio-release.storage.googleapis.com/charts
helm repo add calico https://docs.tigera.io/calico/charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# 2. Dependency 빌드
helm dependency build common-charts/infra/istio/base
helm dependency build common-charts/infra/calico
helm dependency build common-charts/infra/monitoring/loki

# 3. charts/ 폴더 커밋
git add common-charts/**/charts/
git commit -m "chore(helm): update chart dependencies"
git push

왜 주기적으로 해야 하는가?#

  1. 버전 업그레이드: upstream chart 버전이 올라가면 Chart.yaml 수정 후 rebuild 필요
  2. Chart.lock 불일치: 로컬과 원격 간 lock 파일 불일치 시 rebuild
  3. 캐시 문제: ArgoCD repo-server 캐시가 오래된 경우

자동화 방안 (TODO)#

  • CI/CD에서 helm dependency build 자동 실행
  • 또는 ArgoCD Application에 helm.skipCrds: true + pre-sync hook

6. EnvoyFilter dynamic metadata 이슈#

문제#

  • EnvoyFilter의 envoy_on_response에서 :authority 헤더 접근 시 nil 반환

원인#

  • :authority요청 헤더이며, 응답 단계에서 접근 불가
  • Envoy Lua 필터에서 요청 정보를 응답 단계로 전달하려면 dynamic metadata 사용 필요

해결#

-- 요청 단계: 메타데이터에 저장
function envoy_on_request(request_handle)
  local host = request_handle:headers():get(":authority") or ""
  local path = request_handle:headers():get(":path") or "/"

  if string.find(host, "swagger") then
    request_handle:streamInfo():dynamicMetadata():set("swagger_filter", "is_swagger", "true")
    request_handle:streamInfo():dynamicMetadata():set("swagger_filter", "original_path", path)
  end
end

-- 응답 단계: 메타데이터에서 읽기
function envoy_on_response(response_handle)
  local metadata = response_handle:streamInfo():dynamicMetadata():get("swagger_filter")

  if metadata and metadata["is_swagger"] == "true" then
    -- 처리 로직
  end
end

7. Istio IngressGateway 라우팅 싱크 문제#

문제#

  • VirtualService/EnvoyFilter 변경 후 라우팅이 즉시 반영 안 됨
  • istioctl proxy-status에서 SYNCED인데도 404 발생

원인#

  • Istiod → IngressGateway 간 config push 지연
  • 여러 VirtualService가 같은 host를 참조할 때 충돌 가능성

해결#

# IngressGateway 재시작 (가장 확실)
kubectl rollout restart deployment istio-ingressgateway -n istio-system

# 또는 Istiod 재시작 (모든 프록시에 config 재전송)
kubectl rollout restart deployment istiod -n istio-system

예방#

  • 불필요한 VirtualService 정리 (faro-receiver 등)
  • 삭제된 서비스를 참조하는 VirtualService 제거

커밋#

fix(grafana): standardize ArgoCD dashboard structure and fix Unhealthy query
feat(istio): add swagger 403→login redirect EnvoyFilter
fix(istio): use dynamic metadata for EnvoyFilter cross-phase header access
chore(helm): add chart dependencies for istio, calico, loki