예약 동기화를 켜려는데 시스템이 단호하게 말했다.

Credential을 저장해야 Schedule을 사용할 수 있습니다.

처음 보면 맞는 말처럼 들린다.

사람이 직접 실행하지 않는다면, 서버가 나중에 쓸 인증정보를 가지고 있어야 하니까.

그런데 인증 방식이 AWS Cross Account Role이라면 이야기가 달라진다. 이 방식은 Access Key를 저장하지 않아도 예약 실행이 가능하다. 실행할 때마다 AWS STS에서 임시 자격 증명을 새로 받기 때문이다.

그렇다면 시스템이 본 false는 무엇이었을까?

정적 Credential을 저장하지 않았다.

시스템이 내린 결론은 무엇이었을까?

나중에 인증할 방법이 없다.

두 문장은 비슷해 보이지만 같은 말이 아니다.

오늘은 이 작은 차이가 어떻게 자동화 기능을 막는 버그가 되는지, 그리고 인증 로직을 어떻게 모델링해야 하는지 쉽게 풀어보려 한다.

새벽 배송에는 두 가지 출입 방식이 있다

새벽 3시에 정기배송 기사가 건물에 들어와야 한다고 해보자.

첫 번째 건물은 공동현관 비밀번호를 사용한다.

기사
→ 저장된 공동현관 번호 입력
→ 건물 입장

자동 방문을 계속하려면 번호를 어딘가에 보관해야 한다. 번호를 저장하지 않으면 사람이 매번 알려줘야 한다.

두 번째 건물은 안내 데스크가 있는 건물이다.

기사
→ 예약번호와 신원 제시
→ 안내 데스크가 당일 임시 출입권 발급
→ 건물 입장

여기서는 영구 출입번호를 보관할 필요가 없다. 방문할 때마다 임시 출입권을 새로 받으면 된다.

클라우드 인증도 비슷하다.

방식 배송 비유 자동 실행에 필요한 것
Access Key 저장된 공동현관 번호 저장된 Key
Instance Role 건물 상주 직원 신분 실행 환경의 Role
Cross Account Role 안내 데스크의 임시 출입권 Role 정보와 STS 호출 경로

중요한 건 “열쇠를 저장했는가?”가 아니다.

미래의 실행 시점에도 다시 인증할 수 있는가?

이 질문이 진짜다.

인증에는 사실 세 개의 축이 있다

인증 문제를 어렵게 만드는 가장 흔한 이유는 서로 다른 질문을 하나로 뭉치는 것이다.

최소한 세 축을 분리해야 한다.

1. 어떤 방식으로 인증하는가?

Access Key
Instance Profile
Cross Account Role
Profile Credential

이건 인증의 종류다.

2. 어떤 정보를 저장하는가?

Access Key와 Secret Key를 저장하는가?
Role ARN을 저장하는가?
External ID를 저장하는가?
실행 환경의 기본 Credential을 사용하는가?

이건 인증 재료의 보관 방식이다.

3. 언제 실행하는가?

관리자가 지금 직접 실행하는가?
Scheduler가 10분마다 실행하는가?

이건 작업의 실행 방식이다.

세 축은 관련은 있지만 동일하지 않다.

인증 방식 ≠ 저장 방식 ≠ 실행 방식

Access Key에서는 저장 여부와 예약 실행 가능 여부가 강하게 연결된다.

하지만 Cross Account Role에서는 정적 Key 저장 여부가 예약 실행 가능 여부를 결정하지 않는다.

같은 Boolean을 모든 인증 방식에 적용하면 여기서 사고가 난다.

Cross Account Role은 어떻게 비밀번호 없이 일할까?

AWS Cross Account Role의 핵심은 AssumeRole이다.

서비스가 다른 AWS Account의 리소스를 조회해야 한다고 해보자. 다른 Account의 장기 Access Key를 복사해 보관하는 대신, 대상 Account가 허용한 Role을 잠시 맡는다.

흐름은 이렇다.

Scheduler 실행
  ↓
저장된 Role ARN과 External ID 확인
  ↓
AWS STS AssumeRole 호출
  ↓
짧게 유효한 임시 Credential 발급
  ↓
임시 Credential로 AWS API 호출
  ↓
작업 완료

STS는 AWS Security Token Service다.

쉽게 말하면 “이 Role을 잠시 맡아도 되는지 확인하고, 유효기간이 짧은 임시 자격 증명을 발급하는 서비스”다.

여기서 장기 Access Key를 저장하지 않았다는 것은 실패가 아니다. 오히려 장기 secret을 여러 시스템에 복사하지 않기 위한 설계일 수 있다.

장기 Key 저장
→ 같은 Key를 반복 사용

AssumeRole
→ 실행할 때마다 임시 Credential 발급

둘 다 자동 실행할 수 있다.

방법이 다를 뿐이다.

버그는 아주 그럴듯한 한 줄에서 시작된다

이제 흔히 볼 수 있는 validation을 보자.

if (scheduleEnabled && !savedStaticCredential) {
    throw ValidationException(
        "Credential must be saved for scheduling"
    )
}

코드는 짧고 읽기 쉽다.

Access Key만 생각하면 맞는 코드다.

문제는 savedStaticCredential=false인 이유가 여러 가지라는 점이다.

A. Access Key를 입력했지만 저장하지 않음
B. Instance Role이라 저장할 static key 자체가 없음
C. Cross Account Role이라 STS 임시 credential을 사용함
D. Profile 기반 인증정보를 별도 모델로 보관함

Boolean 하나가 서로 다른 상태를 납작하게 눌러버렸다.

그리고 validation은 이 모든 상태를 같은 실패로 처리했다.

static key 저장 안 함
→ 미래에 인증 불가
→ Schedule 차단

첫 번째 화살표가 모든 인증 방식에서 참이 아니었다.

더 나은 질문으로 바꾸면 코드도 달라진다

기존 질문은 이랬다.

정적 Credential을 저장했는가?

더 나은 질문은 이렇다.

이 인증 방식은 예약 실행을 위해 저장된 정적 Credential이 필요한가?

의사코드로 바꾸면 다음과 같다.

val requiresStoredStaticCredential = when (credentialType) {
    ACCESS_KEY -> true
    SERVICE_ACCOUNT -> true
    CLIENT_SECRET -> true
    INSTANCE_PROFILE -> false
    CROSS_ACCOUNT_ROLE -> false
    PROFILE_CREDENTIAL -> false
}

if (
    scheduleEnabled &&
    requiresStoredStaticCredential &&
    !savedStaticCredential
) {
    throw ValidationException(
        "This credential type must be saved for scheduling"
    )
}

조건문은 조금 길어졌다.

대신 도메인의 의미가 코드에 들어왔다.

이 차이는 중요하다.

짧은 코드는 항상 단순한 코드가 아니다. 도메인 차이를 지워서 짧아진 코드는 나중에 더 비싼 설명서를 요구한다.

Create는 되는데 Update가 안 되는 기묘한 세계

이런 버그가 더 헷갈리는 이유는 생성과 수정 경로가 서로 다른 validation을 쓰기 때문이다.

예를 들어 생성에서는 인증 방식까지 확인했다고 해보자.

Cross Account Role + Schedule
→ 생성 성공

그런데 수정에서는 generic Boolean만 확인한다.

기존 Cross Account Role 유지
기존 Schedule 유지
태그만 수정
→ Credential 저장 오류

사용자는 당황한다.

“태그 하나 바꾸는데 왜 인증정보를 다시 저장하라는 거지?”

좋은 질문이다.

태그 수정은 Credential 변경이 아니다. 수정 요청에 Credential 필드가 아예 없을 수도 있다. 시스템은 기존 인증정보를 보존하고 요청된 설정만 바꾸면 된다.

여기서 또 하나의 교훈이 나온다.

Partial Update의 validation은 요청에 들어온 값뿐 아니라, 기존 값과 합쳐진 최종 상태를 검증해야 한다.

하지만 최종 상태를 검증한다고 해서 모든 필드를 다시 요구하면 안 된다.

기존 상태 + 변경 요청
→ 최종 상태 계산
→ 최종 상태의 도메인 규칙 검증
→ 변경 대상만 저장

이 순서가 중요하다.

Manual과 Schedule도 서로 다른 계약이다

Access Key를 저장하지 않는 선택 자체가 항상 잘못은 아니다.

관리자가 수동 동기화를 실행하면서 그때만 Key를 전달할 수 있다.

Manual Sync
→ 현재 요청에 포함된 Key 사용
→ 작업 후 장기 보관하지 않음

이건 충분히 유효한 보안 정책이다.

하지만 Scheduler는 실행 시점에 사람이 없다.

Schedule Sync
→ 새벽 3시 실행
→ 요청을 보낸 사람 없음
→ 저장된 Key도 없음
→ 인증 불가

따라서 Access Key 미저장 상태에서 Schedule을 차단하는 것은 정상이다.

반면 Cross Account Role은 사람이 없어도 STS를 다시 호출할 수 있다.

Schedule Sync
→ 새벽 3시 실행
→ runtime credential로 STS 호출
→ 임시 Credential 발급
→ 인증 가능

결론은 “Schedule이면 Credential을 저장해야 한다”가 아니다.

Schedule이면
실행 시점에 재현 가능한 인증 경로가 있어야 한다.

이 표현이 훨씬 정확하다.

테스트도 인증 방식별 표로 만들어야 한다

이런 validation은 happy path 하나로는 지키기 어렵다.

테스트를 인증 방식과 실행 방식의 행렬로 만들어야 한다.

Credential Type Manual Schedule
Access Key, 미저장 허용 차단
Access Key, 저장 허용 허용
Instance Role 허용 허용
Cross Account Role 허용 허용
Profile Credential 허용 허용

Update도 따로 확인한다.

Cross Account Role + Schedule
  + 이름만 변경
  → 성공

Cross Account Role + Schedule
  + 태그만 변경
  → 성공

Cross Account Role + Schedule
  + 포트만 변경
  → 성공

Access Key 미저장 + Manual
  + Schedule로 변경
  → 실패

여기서 중요한 테스트는 “인증정보를 바꿨을 때”만이 아니다.

인증정보를 바꾸지 않았을 때도 기존 상태가 정상적으로 유지되는지 확인해야 한다.

설정 API의 회귀는 unrelated field를 수정할 때 자주 드러난다.

Boolean은 사실을 말하지만 맥락은 말하지 않는다

savedStaticCredential=false는 거짓말이 아니다.

정적 Credential을 저장하지 않았다는 사실을 정확히 말한다.

문제는 그 사실에 너무 많은 의미를 붙였다는 것이다.

false
→ 저장 안 함
→ 인증정보 없음
→ 자동 실행 불가
→ 잘못된 설정

도메인이 복잡해질수록 Boolean 하나에서 이런 추론 사슬을 만들면 위험하다.

더 나은 모델은 capability를 직접 표현한다.

canAuthenticateUnattended
requiresStoredSecret
supportsScheduledExecution
usesEphemeralCredential

또는 인증 방식 자체가 행동을 대답하게 할 수 있다.

credentialPolicy.supportsSchedule()
credentialPolicy.requiresStoredSecret()
credentialPolicy.createRuntimeCredential()

이렇게 하면 “저장 여부”라는 구현 세부가 “예약 가능 여부”라는 제품 정책을 대신 결정하지 않는다.

보안은 저장량이 아니라 수명주기다

Credential을 적게 저장하는 것은 대체로 좋은 방향이다.

하지만 “아무것도 저장하지 않는다”만으로 보안이 완성되지는 않는다.

Cross Account Role도 다음 정보와 정책이 필요하다.

어떤 Role을 맡을 수 있는가?
누가 AssumeRole을 호출할 수 있는가?
External ID를 어떻게 검증하는가?
임시 Credential의 수명은 얼마인가?
권한은 최소 범위인가?
호출과 실패가 감사로그에 남는가?

Access Key 저장 방식도 마찬가지다.

암호화해서 저장하는가?
누가 복호화할 수 있는가?
교체와 폐기는 어떻게 하는가?
노출 시 영향 범위는 어디까지인가?

보안은 “저장했나, 안 했나”의 이분법이 아니다.

발급, 저장, 사용, 갱신, 만료, 폐기의 전체 수명주기다.

작은 오류 메시지가 가르쳐준 것

처음에는 단순한 validation 오류처럼 보였다.

Credential을 저장해야 Schedule을 사용할 수 있습니다.

하지만 이 한 줄을 따라가 보니 인증 설계의 중요한 원칙들이 나왔다.

1. 정적 Credential 저장과 자동 인증은 같은 개념이 아니다

STS처럼 실행 시점에 임시 Credential을 발급하는 방식이 있다.

2. 인증 방식, 저장 방식, 실행 방식을 분리해야 한다

세 축을 하나의 Boolean으로 표현하면 정상 조합을 차단하기 쉽다.

3. Create와 Update는 같은 최종 상태에 같은 정책을 적용해야 한다

생성 가능한 설정이 단순 수정에서 거부된다면 validation 계약을 비교해야 한다.

4. Partial Update는 합쳐진 최종 상태를 검증해야 한다

그렇다고 변경하지 않은 Credential을 다시 입력하라고 요구해서는 안 된다.

5. 테스트는 Credential Type × 실행 방식의 행렬이어야 한다

Access Key 한 종류의 규칙을 전체 인증 방식으로 일반화하면 안 된다.

자동화가 기억해야 할 것은 비밀번호가 아니다

자동화 시스템은 사람이 없는 시간에도 움직여야 한다.

그래서 무언가를 기억해야 한다.

하지만 그 무언가가 반드시 비밀번호나 장기 Access Key일 필요는 없다.

저장된 secret일 수도 있고,
실행 환경의 Role일 수도 있고,
임시 Credential을 발급받는 신뢰 관계일 수도 있다.

자동화가 정말 기억해야 할 것은 값 하나가 아니다.

다음 실행에서도 안전하게 다시 인증할 수 있는 경로다.

이 차이를 모델에 담으면 불필요한 secret 저장을 줄일 수 있다. 정상적인 예약 작업을 막는 오류도 피할 수 있다. 무엇보다 인증 코드가 현실의 여러 방식을 억지로 한 줄에 눌러 담지 않게 된다.

가끔 가장 까다로운 보안 버그는 권한을 너무 많이 열어서가 아니다.

서로 다른 안전한 인증 방식을 똑같은 것으로 취급해서 생긴다.