“분명 예전에 고쳤는데요?”

장애 회의에서 이 말이 나오면 방 안의 온도가 살짝 내려간다.

누군가는 패치가 되돌아갔다고 생각한다. 누군가는 배포가 잘못됐다고 의심한다. 급한 마음에 Git 로그에서 revert부터 찾는다.

그런데 이번에는 되돌린 사람이 없었다.

예전에 잠근 문은 여전히 잘 잠겨 있었다. 문제는 새 버전에서 옆에 문을 하나 더 만들었는데, 거기에 같은 자물쇠를 달지 않았다는 것이었다.

회귀는 삭제로만 생기지 않는다.

새로운 경로가 오래된 운영 조건을 잊을 때도 생긴다.

로그인은 되는데 연결은 안 된다

증상은 묘했다.

웹 로그인: 성공
SSO 인증: 성공
데스크톱 에이전트 실행: 성공
실제 리소스 연결: 실패

비밀번호가 틀린 것도 아니었다. 사용자 권한도 살아 있었다. 웹 화면도 잘 열렸다.

그런데 에이전트가 실제 연결을 시작하려고 하면 여러 종류의 리소스가 한꺼번에 실패했다.

이럴 때 각 리소스 설정을 하나씩 의심하기 쉽다.

DB 설정이 잘못됐나?
서버 방화벽 문제인가?
인증서가 만료됐나?
대상 시스템이 죽었나?

하지만 서로 다른 목적지가 동시에 같은 버전부터 실패한다면, 목적지보다 먼저 봐야 할 것이 있다.

모두가 공유하는 출발 절차다.

공항에서 부산행, 제주행, 도쿄행 비행기가 동시에 못 뜬다면 각 도시의 날씨보다 탑승 수속대를 먼저 보는 것과 같다.

새 보안 경로가 하나 생겼다

이전 버전의 에이전트는 연결에 필요한 짧은 인증 정보를 로컬에서 만들었다.

에이전트
  -> 로컬에서 인증 정보 생성
      -> 연결 시작

그런데 보안을 더 강화하려면 중요한 서명 재료를 사용자 PC에 두지 않는 편이 좋다. 그래서 새 구조에서는 서버가 인증 정보를 서명하도록 바뀌었다.

에이전트
  -> 서버에 서명 요청
      -> 서명된 인증 정보 수신
          -> 연결 시작

좋은 변화다.

문제는 두 번째 구조에 HTTP 요청이 하나 새로 생긴다는 점이다.

그리고 새 코드는 아주 평범하게 생겼다.

var client = new HttpClient();

이 한 줄 자체는 틀리지 않았다.

하지만 기업용 VDI에서는 평범한 HttpClient가 생각보다 많은 짐을 등에 지고 출발한다.

AWS CLI에게만 알려준 길을 모두가 봤다

기업 VDI에는 종종 외부 통신용 Proxy가 설정되어 있다.

예를 들어 AWS CLI만 사내 Web Proxy를 거치게 만들고 싶다고 해보자.

AWS CLI
  -> 사내 Web Proxy
      -> 인터넷

운영자는 다음과 같은 환경변수를 설정할 수 있다.

HTTPS_PROXY=http://proxy.example:8080

주소는 예시다.

여기서 함정은 HTTPS_PROXY에 “AWS CLI 전용”이라는 꼬리표가 없다는 것이다.

같은 사용자 환경에서 실행된 다른 프로그램도 이 값을 읽을 수 있다. .NET의 기본 HTTP 처리 역시 운영체제와 환경에 따라 System Proxy나 Proxy 환경변수를 사용할 수 있다.

그래서 실제 흐름은 이렇게 달라질 수 있다.

원래 의도

데스크톱 에이전트
  -> 서비스 서버
실제 가능 경로

데스크톱 에이전트
  -> 사내 Web Proxy
      -> 서비스 서버

Proxy가 목적지를 허용하면 아무 일도 없다.

하지만 인증을 요구하거나, 목적지를 차단하거나, TLS를 중간에서 검사하다가 실패하면 이야기가 달라진다.

403     목적지 또는 요청 차단
407     Proxy 인증 필요
Timeout 목적지 연결 지연 또는 실패
TLS 오류 인증서 검사·중간 복호화 문제

로그인은 다른 통신 경로를 사용하니 성공할 수 있다. 하지만 실제 연결 전에 필요한 새 서명 요청은 Proxy에서 막힌다.

결국 사용자 눈에는 이렇게 보인다.

문은 열렸는데 엘리베이터가 움직이지 않는다.

“패치가 원복됐다”는 말의 함정

과거에도 비슷한 Proxy 문제가 있었다고 해보자.

그때는 특정 에이전트의 HTTP Handler에 이런 정책을 넣었다.

handler.UseProxy = false;

코드를 확인해 보니 이 설정은 그대로 남아 있었다.

그렇다면 왜 문제가 돌아왔을까?

적용 대상이 달랐기 때문이다.

과거 패치
  -> 기존 에이전트의 기존 HTTP Handler

새 기능
  -> 데스크톱 에이전트의 새 signing HttpClient

자물쇠는 사라지지 않았다.

새로 만든 문에 자물쇠가 없었다.

이 차이는 꽤 중요하다. “누가 패치를 지웠나?”를 찾으면 사람과 커밋을 추적하게 된다. 반면 “새 호출 경로가 기존 운영 정책을 상속했나?”를 물으면 구조를 추적하게 된다.

대개 두 번째 질문이 더 빨리 원인에 도착한다.

가장 강력했던 실험은 코드가 아니었다

코드 이력은 좋은 가설을 만들었다.

하지만 가설을 원인으로 승진시키려면 현장 증거가 필요하다.

가장 단순한 실험은 A/B 비교였다.

A. 기존 Proxy 정책
   -> 연결 실패

B. 서비스 도메인만 Proxy bypass
   -> 연결 성공

같은 사용자, 같은 VDI, 같은 버전, 같은 목적지에서 Proxy 경로만 바꾼다.

A는 실패하고 B는 성공한다면 Proxy 영향 가능성이 매우 높다. Proxy 로그에 에이전트 요청의 차단 기록까지 남아 있다면 더 강한 직접 증거가 된다.

반대로 bypass 후에도 똑같이 실패한다면 Proxy를 범인으로 확정하면 안 된다. 로그인 초기화, 권한, tunnel, 방화벽처럼 다른 경계를 계속 조사해야 한다.

좋은 디버깅은 가설을 사랑하지 않는다.

가설을 쉽게 틀리게 만들 수 있는 실험을 사랑한다.

무조건 Proxy를 끄면 해결될까?

여기서 흔히 나오는 긴급 수정은 이렇다.

var handler = new SocketsHttpHandler
{
    UseProxy = false
};

새 서명 요청에만 전용 Handler를 쓰면 영향 범위를 작게 만들 수 있다.

하지만 이것을 제품 전체의 영원한 정답으로 삼으면 또 다른 고객이 막힐 수 있다. 어떤 VDI는 서비스 서버에 접근할 때도 반드시 Proxy를 거쳐야 하기 때문이다.

그래서 장기적으로 필요한 것은 truefalse 하나가 아니다.

System Proxy
  운영체제와 환경변수 정책을 따른다

Proxy Disabled
  직접 연결한다

Custom Proxy
  관리자가 지정한 Proxy를 사용한다

Bypass List
  특정 서비스 도메인만 직접 연결한다

네트워크가 복잡한 기업 환경에서는 “Proxy를 쓰는가?”보다 “어떤 통신에 어떤 Proxy 정책을 쓰는가?”가 더 정확한 질문이다.

테스트는 요청 내용만 보고 있었다

새 기능에는 테스트가 있었다.

  • 요청 URL이 맞는지
  • body가 맞는지
  • 응답 token을 처리하는지
  • HTTP 오류를 처리하는지

그런데 테스트용 HttpMessageHandler를 직접 주입하면 실제 운영체제의 Proxy 선택은 건너뛰게 된다.

단위 테스트는 초록색이었지만, VDI의 공용 안내판을 읽는 순간은 시험하지 않았다.

이런 종류의 변경에는 한 단계 더 필요한 이유다.

1. 유효하지 않은 HTTPS_PROXY 설정
2. Direct mode의 전용 Handler
3. 실제 요청 실행
4. Proxy가 아니라 목적지로 직접 가는지 확인

Proxy mode가 여러 개라면 각각의 계약을 테스트해야 한다.

System   -> System Proxy 사용
Disabled -> Proxy 사용 안 함
Custom   -> 지정 Proxy 사용
Bypass   -> 지정 도메인 직접 연결

가능하면 Windows CI나 실제 VDI와 비슷한 통합 환경에서도 확인한다.

환경변수는 코드 리뷰 화면에 잘 보이지 않는다. 그래서 환경변수와 OS 설정이 중요한 기능은 코드만 테스트해서는 부족하다.

이번 사건이 남긴 다섯 가지 규칙

첫째, 마지막 정상 버전과 첫 실패 버전 사이의 새 네트워크 경로를 찾자.

코드가 삭제됐는지만 보지 말고 새로운 API 호출, 새 SDK, 새 Handler, 새 background job이 생겼는지 본다.

둘째, 인증 성공과 연결 성공을 같은 단계로 보지 말자.

로그인, control-plane, data-plane은 서로 다른 길일 수 있다.

셋째, 기업 VDI의 Proxy는 숨은 전역 의존성이다.

AWS CLI용 설정이라고 불려도 같은 세션의 다른 프로세스가 읽을 수 있다.

넷째, 단기 패치는 가장 좁은 Handler에 적용하자.

전역 Proxy를 바꾸면 잘 돌아가던 다른 통신까지 흔들 수 있다.

다섯째, 운영 정책도 테스트 계약에 넣자.

네트워크 정책도 호출 경로의 일부다.

버그는 같은 얼굴로 다른 문에서 들어온다

회귀를 보면 우리는 본능적으로 과거 패치가 사라졌다고 생각한다.

하지만 시스템은 계속 자란다. 새로운 보안 경로가 생기고, 새로운 HTTP Client가 생기고, 새로운 background 요청이 생긴다.

그때 과거에 배운 운영 조건이 함께 이동하지 않으면, 버그는 돌아온 것처럼 보인다.

사실은 돌아온 게 아니다.

같은 얼굴로 새 문을 찾아 들어온 것이다.

다음번에 “분명 예전에 고쳤는데요?”라는 말을 듣는다면 revert만 찾지 말자.

먼저 이렇게 물어보자.

새로 생긴 길은 무엇이고,
그 길에도 예전의 안전장치가 있는가?