1. 비밀번호 대신 토큰을 써도 영원히 유효하면 위험은 남는다

개발자는 Git 저장소를 내려받고 이슈를 조회하고 코드를 올릴 때 서비스에 자신이 누구인지 증명해야 합니다. 매번 비밀번호를 보내는 대신 특정 권한을 가진 토큰을 사용하는 경우가 많습니다. 토큰은 편리하지만 복사된 문자열이므로 로그, 환경변수 출력, 잘못 올린 설정 파일을 통해 유출될 수 있습니다. 만료되지 않는 토큰은 사용자가 직접 폐기할 때까지 공격자도 계속 시도할 수 있습니다.

2026년 10월 21일 GitHub는 GitHub CLI 2.103.0부터 만료되고 자동 갱신되는 OAuth 자격 증명을 공개 미리보기로 선택할 수 있다고 발표했습니다. `gh auth login --expiring`으로 로그인하면 CLI가 만료되는 액세스 토큰과 회전하는 리프레시 토큰을 보관하고, 필요할 때 액세스 토큰을 새로 받습니다. 현재는 선택 기능이며 기존 로그인과 수동 토큰의 동작은 바뀌지 않습니다.

핵심 보안 효과는 유출을 완전히 막는 것이 아니라 유출된 액세스 토큰이 쓸 수 있는 시간을 제한하는 데 있습니다. GitHub 문서는 액세스 토큰이 몇 시간 안에 만료된다고 설명하며 OAuth 문서에는 만료형 액세스 토큰의 기본 수명이 8시간이라고 명시합니다. 리프레시 토큰은 더 오래 유지되므로 액세스 토큰보다 더 엄격하게 보관해야 합니다.

근거 자료 · GitHub Changelog · GitHub CLI: Opt in to expiring, auto-refreshing OAuth tokens · GitHub Docs · Authorizing OAuth apps

2. 액세스 토큰과 리프레시 토큰은 역할이 다르다

액세스 토큰은 API 요청에 직접 붙여 사용자의 권한으로 작업하게 하는 열쇠입니다. 리프레시 토큰은 API 데이터에 직접 접근하는 열쇠가 아니라 새 액세스 토큰을 발급받는 데 사용합니다. 사용자가 처음 승인하면 두 토큰과 각각의 만료 정보가 발급되고, CLI는 액세스 토큰이 필요할 때 현재 상태를 확인해 갱신합니다.

GitHub OAuth 문서에 따르면 리프레시 요청이 성공하면 새 액세스 토큰과 새 리프레시 토큰이 함께 발급되며, 사용한 리프레시 토큰과 이전 액세스 토큰은 더 이상 동작하지 않습니다. 이것이 회전입니다. 여러 프로세스가 같은 리프레시 토큰을 동시에 사용하면 한쪽이 먼저 회전시킨 뒤 다른 쪽이 실패할 수 있으므로 갱신은 한 곳에서 순차 처리하고 새 쌍을 원자적으로 저장해야 합니다.

리프레시 토큰이 만료되거나 폐기되면 자동 갱신으로 해결할 수 없습니다. 사용자가 웹 흐름이나 기기 흐름으로 다시 로그인해야 합니다. 따라서 ‘자동 갱신’은 영구 인증이 아니라 정상적인 토큰 수명 동안 사용자 경험을 유지하는 절차입니다. 로그아웃, 권한 취소, 계정 정책 변경 같은 사건도 항상 고려해야 합니다.

2. 액세스 토큰과 리프레시 토큰은 역할이 다르다
구분주요 용도노출 시 영향보관 원칙
액세스 토큰GitHub API와 Git 작업 인증만료 전까지 허용된 작업 가능짧게 사용하고 출력·로그 금지
리프레시 토큰새 토큰 쌍 발급새 액세스 토큰 획득 가능키링·비밀 저장소에서 엄격히 보호
기기 코드CLI 로그인 승인 연결유효 시간 안에 승인 흐름 악용 가능화면 안내 외 저장 금지
권한 범위토큰이 할 수 있는 작업 제한범위가 넓을수록 피해 확대필요한 범위만 요청

근거 자료 · GitHub Docs · Authorizing OAuth apps

3. CLI 로그인은 브라우저가 없는 도구를 위한 기기 흐름을 이해하게 한다

명령줄 프로그램은 웹사이트처럼 고정된 화면과 콜백 주소를 갖기 어렵습니다. OAuth 기기 흐름에서는 CLI가 기기 코드와 사용자용 코드를 요청하고, 사용자에게 브라우저에서 코드를 입력하도록 안내합니다. CLI는 정해진 간격으로 승인 여부를 확인한 뒤 사용자가 허용하면 토큰을 받습니다. 비밀번호를 터미널 프로그램에 직접 넘기지 않아도 되는 구조입니다.

GitHub 문서의 예시에서는 사용자 코드와 기기 코드가 15분 뒤 만료되고, 서버가 돌려준 최소 확인 간격을 지켜야 합니다. 너무 자주 확인하면 `slow_down` 오류가 발생하고 다음 확인 간격이 늘어납니다. 아직 사용자가 승인하지 않은 `authorization_pending`, 코드 만료, 승인 거절을 서로 다른 상태로 처리해야 합니다.

로그인 성공만 구현하면 운영 중 문제가 생깁니다. 네트워크가 끊기거나 사용자가 다른 계정으로 승인하거나 리프레시 토큰이 만료되는 경우가 있습니다. 매 로그인과 갱신 뒤 API에서 사용자 식별자를 다시 확인하고, 계정 데이터가 섞이지 않도록 로컬 프로필과 서버 계정을 정확히 연결해야 합니다.

# 교육용 의사코드: 실제 비밀값은 출력하지 않는다
device = request_device_code(client_id, scopes)
show_user_code(device.user_code, device.verification_uri)

while now() < device.expires_at:
    wait(device.interval)
    result = poll_authorization(device.device_code)
    if result.status == 'slow_down':
        device.interval = result.next_interval
    elif result.status == 'approved':
        encrypted_store.rotate(result.token_pair)
        verify_user_identity(result.access_token)
        break

4. 자동 갱신에서 가장 중요한 것은 회전의 원자성이다

서비스가 API 호출 직전에 액세스 토큰 만료를 확인하고 충분한 시간이 남았다면 그대로 사용합니다. 만료됐거나 곧 만료될 예정이면 하나의 갱신 작업만 리프레시 토큰을 사용하게 잠급니다. 서버 응답으로 새 토큰 쌍을 받으면 두 값을 한 번에 저장하고, 기다리던 요청은 새 액세스 토큰으로 다시 시도합니다.

새 액세스 토큰만 저장하고 새 리프레시 토큰 저장에 실패하면 다음 갱신이 불가능해질 수 있습니다. 반대로 여러 서버가 예전 리프레시 토큰을 동시에 사용하면 한 서버의 회전 뒤 다른 서버는 `bad_refresh_token`을 받을 수 있습니다. SQL 트랜잭션이나 분산 잠금, 버전 번호를 사용해 한 번의 회전만 성공하도록 설계할 수 있습니다.

GitHub CLI를 사용하는 일반 개발자는 이 로직을 직접 만들지 않아도 됩니다. CLI가 자격 증명 갱신을 맡도록 두고 `gh auth token` 결과를 파일이나 환경변수에 오래 복사해 두지 않는 것이 중요합니다. 외부 도구에서 토큰이 필요하면 사용할 때마다 새로 읽어야 CLI가 필요한 경우 먼저 갱신할 수 있습니다.

-- 토큰 원문이 아닌 상태·버전 중심의 교육용 스키마
CREATE TABLE credential_state (
  account_id BIGINT PRIMARY KEY,
  token_version BIGINT NOT NULL,
  access_expires_at TIMESTAMP NOT NULL,
  refresh_expires_at TIMESTAMP,
  encrypted_secret_ref VARCHAR(300) NOT NULL,
  updated_at TIMESTAMP NOT NULL
);

-- 갱신 시 token_version을 조건으로 비교·교환하고
-- 비밀값 자체는 로그나 일반 테이블에 기록하지 않는다.

5. Git과 스크립트는 토큰을 한 번 복사해 두는 습관을 바꿔야 한다

GitHub는 HTTPS Git 작업에 만료형 토큰을 쓸 때 `gh auth setup-git`으로 CLI를 자격 증명 도우미로 설정할 것을 권장합니다. 그러면 Git이 필요할 때 CLI가 현재 유효한 자격 증명을 제공합니다. Git 2.46 이상에서는 CLI가 만료 정보를 전달해 다른 캐시 도우미가 수명보다 오래 토큰을 보관하지 않도록 할 수 있습니다.

반면 스크립트 시작 때 `gh auth token`을 한 번 실행해 값을 파일에 저장하고 며칠 동안 재사용하면 언젠가 만료됩니다. 실패 원인을 네트워크 오류로 오해해 무한 재시도할 수도 있습니다. 각 작업 직전에 토큰을 읽거나, 가능하면 토큰 값을 다루지 않고 `gh api`처럼 갱신을 관리하는 도구를 사용합니다.

자동화 환경은 개인 CLI 로그인과 구분해야 합니다. GitHub Docs는 스크립트에 환경변수 토큰을 제공하고 GitHub Actions에서는 워크플로에 제공되는 `GITHUB_TOKEN`을 `GH_TOKEN`으로 전달하는 방식을 설명합니다. 개인의 장기 토큰을 서버에 복사하는 대신 작업 주체와 저장소 범위에 맞는 자격 증명을 선택해야 합니다.

5. Git과 스크립트는 토큰을 한 번 복사해 두는 습관을 바꿔야 한다
상황적합한 방식피해야 할 방식
개발자 PC의 gh키링과 자동 갱신 자격 증명토큰을 메모장·셸 기록에 저장
Git over HTTPSgh credential helper만료를 모르는 별도 캐시에 장기 보관
GitHub Actions작업에 제공되는 GITHUB_TOKEN개인 토큰을 저장소 비밀로 무기한 재사용
별도 서버 자동화비밀 저장소와 전용 앱·최소 권한개발자 계정 자격 증명 공유

근거 자료 · GitHub Docs · Authenticating with GitHub CLI · GitHub Docs · GITHUB_TOKEN

6. Python·Java·SQL·클라우드로 토큰 없이 토큰 시스템을 실습한다

다음 프로젝트는 실제 GitHub 토큰을 수집하는 실습이 아닙니다. Python으로 가짜 액세스 토큰과 리프레시 토큰의 상태 전이를 모의하고, 현재 시각·만료 시각·토큰 버전을 입력받아 사용, 갱신, 재로그인 중 하나를 반환합니다. 비밀 문자열 대신 임의 식별자와 해시만 사용해 로그에 원문이 남지 않게 합니다.

Java API는 `/credentials/status`와 `/credentials/refresh`를 제공하되 실제 토큰 값은 응답하지 않습니다. 여러 요청이 동시에 갱신을 시도하는 테스트를 만들어 한 요청만 성공하고 나머지는 새 버전을 다시 읽는지 확인합니다. SQL에는 성공·실패·이유·발생 시각·토큰 버전만 감사 이벤트로 저장합니다.

클라우드 설계에서는 암호화된 비밀 저장소, 서비스 계정 권한, 접근 감사 로그와 만료 경보를 배치합니다. AI 코딩 도구가 로그를 분석하거나 코드를 생성할 때도 토큰 원문을 프롬프트에 넣지 않습니다. AI는 `invalid token` 오류 유형과 수정 후보를 제안할 수 있지만, 새 비밀을 발급하거나 권한을 확대하는 결정은 승인된 운영 절차를 거쳐야 합니다.

사용자 승인 → 토큰 쌍 발급 → 암호화 저장
→ Java API 요청 전 만료 확인
→ 유효: 액세스 토큰 사용
→ 임박/만료: 단일 갱신 잠금
→ 새 토큰 쌍 원자적 교체 + SQL 감사 이벤트
→ 실패: 원인 분류 후 재로그인 또는 운영 경보

7. 짧은 수명만으로 최소 권한과 안전한 저장을 대신할 수 없다

액세스 토큰이 8시간 뒤 만료돼도 그 안에는 허용된 작업을 수행할 수 있습니다. 권한 범위가 넓고 여러 저장소에 접근할 수 있다면 짧은 시간에도 피해가 큽니다. 필요한 권한만 요청하고, 사람이 없는 자동화에는 사용자 토큰보다 세밀한 권한과 짧은 수명을 지원하는 GitHub App 같은 방식을 검토해야 합니다.

리프레시 토큰이 유출되면 새 액세스 토큰을 얻을 수 있으므로 액세스 토큰보다 덜 민감한 값으로 취급해서는 안 됩니다. 운영 로그에는 토큰 전체나 앞뒤 일부를 남기지 않고 비밀 참조 ID와 버전만 기록합니다. 사고가 의심되면 만료를 기다리지 말고 자격 증명을 폐기하고 감사 기록을 확인합니다.

또한 모든 서버가 만료 토큰을 지원한다고 가정하면 안 됩니다. GitHub CLI는 서버가 지원하지 않을 때 일반 비만료 토큰으로 전환할 수 있으며, GitHub Enterprise Server는 3.23부터 발급합니다. 상태 화면과 테스트에서 실제 발급 형태를 확인해야 ‘선택했다’는 설정과 ‘실제로 만료된다’는 결과를 혼동하지 않습니다.

  • 액세스 토큰·리프레시 토큰 원문이 로그에 없는가
  • 권한 범위가 수행할 작업보다 넓지 않은가
  • 동시 갱신에서 한 번만 토큰 쌍이 교체되는가
  • 만료·폐기·네트워크 실패를 서로 구분하는가
  • 서버 미지원 시 비만료 토큰으로 전환됐음을 감지하는가
  • 자동화와 개인 개발 환경의 자격 증명을 분리했는가

8. 2주 실습의 평가는 비밀값이 아니라 실패 복구로 한다

첫 주에는 Python 상태 머신과 SQL 감사 테이블을 만듭니다. 정상 사용, 만료 5분 전, 이미 만료, 리프레시 만료, 동시 갱신이라는 가상 사건을 실행합니다. 각 사건에서 반환 상태와 토큰 버전이 예상대로 바뀌는지 자동 테스트하고, 비밀 원문이 출력되지 않는지 별도 검사합니다.

둘째 주에는 Java API와 클라우드 구성도를 연결합니다. 두 요청이 같은 리프레시 토큰을 동시에 사용하려는 부하 테스트, 비밀 저장소 연결 실패, 시스템 시계 차이와 재로그인 흐름을 검증합니다. 실제 서비스 계정이나 GitHub 토큰 없이도 상태 전이와 장애 처리를 충분히 학습할 수 있습니다.

발표에서는 ‘토큰이 만료되어 안전하다’는 한 문장 대신 공격자가 액세스 토큰을 얻었을 때 가능한 시간과 권한, 리프레시 토큰 보호 방식, 갱신 실패 뒤 복구 절차를 설명합니다. 보안 기능의 가치는 정상 로그인 화면보다 유출과 만료가 발생했을 때 시스템이 얼마나 작고 추적 가능한 피해로 멈추는지에서 확인할 수 있습니다.

8. 2주 실습의 평가는 비밀값이 아니라 실패 복구로 한다
평가 항목통과 조건증거
상태 전이사용·갱신·재로그인이 정확히 구분됨Python 테스트 결과
동시성한 번의 회전만 성공함Java 동시 요청 로그
비밀 보호원문 토큰이 저장·출력되지 않음로그 검사와 스키마
감사 가능성계정·버전·이유·시각을 추적함SQL 감사 이벤트
복구폐기·만료·저장소 장애 절차가 있음운영 흐름도와 실행 기록
LEARNING CHECKPOINT

학생이 이 이슈에서 확인할 것

  • 액세스 토큰과 리프레시 토큰의 역할과 위험을 구분할 수 있나요?
  • 회전 뒤 이전 토큰이 무효화되는 흐름을 설명할 수 있나요?
  • 동시 갱신과 리프레시 만료를 안전하게 처리했나요?
  • 개발자 PC·GitHub Actions·서버 자동화의 인증 방식을 구분했나요?
  • 짧은 수명과 최소 권한·비밀 저장·감사 로그를 함께 설계했나요?

실제 비밀값 없이 만료형 OAuth 토큰 시스템을 모의하세요. Python 상태 머신, SQL 감사 이력, Java 동시 갱신 API와 클라우드 비밀 저장소 구성을 연결하고, 만료·회전·폐기·서버 미지원 상황의 복구 결과를 보고서로 작성하세요.

관심 기술을 대학의 프로젝트로 연결하세요.

전공의 학습 흐름을 이해하고 학생이 직접 만든 결과를 확인한 뒤, 자신에게 맞는 입학 계획을 세워 보세요.