실습 목표
1주차에서 애플리케이션 코드와 보안을 확보했다면 2주차는 코드가 실행되는 환경(Container), 환경을 구축하는 명세서(IaC) 그리고 환경에 주입되는 민감 정보(Secret)을 보호하는데 집중한다.
1. 컨테이너 런타임 요새화 (Docker Hardening)
- 공격 표면 최소화: Distroless 베이스 이미지를 적용하여 셸(Shell), 패키지 관리자 등 불필요한 OS 바이너리를 제거한 최소 실행 환경 구축
- 권한 최소화 (Non-root): 컨테이너 내부 프로세스가 Root 권한이 아닌 별도의 비특권 계정으로 실행되도록 설정하여 컨테이너 탈출 공격 방어
- 최종 무결성 검증 (Trivy): 빌드된 이미지 내 OS 패키지 취약점을 최종 스캔하여 배포 전 High/Critical CVE Zero 달성
2. 코드형 인프라 가드레일 (IaC Security)
- 설정 오류 자동 탐지 (Checkov): Terraform 코드를 분석하여 퍼블릭 버킷 노출, 암호화 미설정, 과도한 보안 그룹 권한 등 설정 오류(Misconfiguration) 원천 차단
- 보안 규정 준수 (Compliance): 인프라 배포 전 클라우드 보안 모범 사례(Best Practice)를 자동 검증하는 정적 분석 단계 구현
3. 제로 트러스트 기밀 관리 (Vault Secrets)
- 중앙 집중식 관리: GitHub Secrets에 분산된 시크릿을 HashiCorp Vault로 통합하고, GitHub은 오직 Vault 접근을 위한 통로역할로 한정
- 시크릿 탈중앙화: 소스 코드 및 설정 파일 내 하드코딩된 모든 평문 시크릿을 제거하고 런타임 시 동적 주입 체계 구축
- 감사 및 생명주기 관리: 시크릿 접근 내역의 가시성을 확보하고, 유효 기간(TTL) 설정을 통한 시크릿 노출 위협 최소화
4. 고도화된 DevSecOps 파이프라인 통합
- 보안 가드레일 자동화: Checkov(인프라)와 Trivy(컨테이너)를 CI/CD에 통합하여 보안 기준 미달 시 빌드/배포를 즉시 차단(Fail-Fast)
- 연쇄적 방어 체계 구축: 애플리케이션(1주차) → 컨테이너(2주차) → 인프라(2주차)로 이어지는 계층적 방어(Defense in Depth) 실현
실습
1. Docker Hardening (with Trivy) + 자동화
목표:
Distroless 이미지를 채택하여 쉘과 패키지 매니저 등 불필요한 OS 도구를 제거함으로써 공격 표면을 근본적으로 최소화한다.
Multi-stage Build를 적용해 빌드와 실행 환경을 분리함으로써 이미지 경량화 및 소스 코드 보호를 달성한다.
Non-root User 설정을 통해 프로세스 실행 권한을 제한하는 하드닝을 수행한다.
Trivy 스캔을 통해 이미지 내 잔존 보안 결함(CVE)을 식별하고 위협 등급별로 관리함으로써, 검증된 이미지인 인프라만을 배포하는 보안 가드레일을 구축한다.
1-1) Trivy 설치 및 기존 이미지 진단
먼저 기존의 이미지가 얼마나 취약한지를 확인하는 과정이 필요하다.
1) Trivy 설치
# 1. 최신 버전 태그(버전 번호) 추출
export TRIVY_VERSION=$(curl -s https://api.github.com/repos/aquasecurity/trivy/releases/latest | grep tag_name | cut -d '"' -f 4 | sed 's/v//')
# 2. 해당 버전의 .deb 패키지 다운로드
wget https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.deb
# 3. 설치 진행
sudo dpkg -i trivy_${TRIVY_VERSION}_Linux-64bit.deb
# 4. 확인
trivy version
2) 기존 이미지 진단
1주차에 Docker hub에 올렸던 이미지를 원격으로 진단한다.
trivy image --severity CRITICAL,HIGH --table-mode summary [docker hub ID]/pygoat:devsecops
- 위험한 취약점만 출력하고 싶으면 --severity 추가
- 표 요약본만 출력하고 싶으면 --table-mode summary 추가

3) 기존 이미지 취약점 분석
- 탐지 로그 요약
- Base OS: Debian 10.12 (취약점 918건 탐지)
- App Library: Django 4.2(19건), Pillow 9.4.0(4건), PyYAML 5.1(3) 등 다수의 Python 패키지 결함 포함
- 진단 : 오래된 OS 버전(Debian 10) 및 패키지 관리 부재로 인한 심각한 보안 수준 미달 상태.
- 위협
- OS 취약점: 패치가 중단된 Debian 10 사용으로 커널 및 시스템 라이브러리 공격을 통한 권한 상승(Privilege Escalation) 위험 상존.
- App 취약점: Django, PyYAML 등 핵심 프레임워크 결함으로 인한 원격 코드 실행(RCE) 및 데이터 유출 가능성 매우 높음.
1-2) Docker Hardening
1) Base Image 교체 (Distroless)
1. 현재 인프라의 통신 및 계층 구조
WSL2 환경에서 컨테이너가 실행될 때, 명령이 전달되는 경로는 다음과 같다.
- 1) 애플리케이션 (PyGoat): 파이썬 코드가 실행됨.
- 2) 이미지 내부 OS (Debian 10): 파이썬 실행에 필요한 라이브러리(.so 파일) 제공.
- 3) WSL2 리눅스 커널: 실제 연산을 처리하는 엔진. 윈도우 커널과 나란히 위치하여 하드웨어 자원을 호출함.
- 4) Hyper-V (하이퍼바이저): 윈도우와 WSL2 커널 사이에서 자원을 배분하는 교통정리 역할.
2. Base Image를 교체해야 하는 3가지 결정적 이유
- 1) 엔진(커널)으로 가는 통로 차단 :컨테이너는 WSL2 리눅스 커널을 공유하는데 이미지 내부에 불필요한 도구(bash, apt 등)가 많으면, 공격자가 이 도구들을 이용해 커널의 취약점을 공격할 가능성이 커진다.
- 2) 윈도우 호스트 보호 : WSL2는 윈도우와 파일 시스템, 네트워크를 밀접하게 공유한다. 이미지 내부에 curl이나 ssh 같은 도구가 살아있다면, 컨테이너를 장악한 해커가 윈도우 파일 시스템으로 접근하거나 내부 네트워크를 스캔하기가 매우 쉬워진다.
- 3) 보안 노이즈(918개) 제거 : 918개의 취약점은 대부분 파이썬과 무관한 Debian OS 패키지들 이다. 이 상태로는 진짜 위험한 취약점이 새로 발견되어도 수많은 리스트에 묻혀 식별이 불가능하다.
| 구분 | 기존 이미지 (Legacy) | 교체 후 이미지 (Hardened) |
| OS 패키지 | 수백 개의 취약한 유틸리티 포함 | 필수 라이브러리 외 전량 삭제 |
| 라이브러리 | 낡고 취약한 버전 방치 | 빌드 단계에서 최신 버전으로 교체 |
| 보안 노이즈 | 918개의 혼란스러운 리스트 | 실제 관리 가능한 0~5개 수준 |
| 공격 수단 | 취약한 라이브러리 + 실행 도구(bash) | (패치된 라이브러리) + (도구 없음) |
2) Multi-stage Build
하나의 Dockerfile 안에서 '빌드 단계(Builder)'와 '실행 단계(Final)'를 물리적으로 분리하여, 최종 이미지에서 보안 위협 요소를 제거하는 전략이다.
- 1단계: Builder Stage (공장)
- 환경: 컴파일러(gcc), 패키지 매니저(pip), 빌드 캐시 등 모든 도구가 포함된 무거운 이미지.
- 역할: 소스 코드를 가져와 라이브러리를 설치하고, 실행 가능한 상태의 '결과물(Artifact)'을 생성.
- 특징: 이 단계에서 최신 이미지를 사용하여 라이브러리 취약점을 패치(Remediation)하며, 작업 완료 후 이 환경은 통째로 삭제됨.
- 2단계: Final Stage (보안 실행 환경)
- 환경: 쉘(bash), apt, curl 등 OS 유틸리티가 전혀 없는 Distroless 이미지.
- 역할: 1단계에서 완성된 결과물(~/.local)만 복사(COPY --from=builder)하여 실행.
3) User-Hardening
컨테이너가 뚫리더라도 호스트 시스템(WSL2/Windows)으로 피해가 확산되지 않도록 '최소 권한 원칙'을 적용한다.
- Root 권한의 위험성: 컨테이너는 호스트의 커널을 공유하므로 내부 프로세스가 Root 권한이면 해커가 커널 취약점을 이용해 호스트로 이동 가능하므로 시스템 콜을 차단하는 일반 사용자 권한(Non-root) 사용하여 윈도우 파일 시스템 및 네크워크 보호 달성
- 프로세스 침투: OS 패키지가 없어도 애플리케이션(PyGoat) 자체의 취약점을 통해 해커가 침투할 수 있으며, 이때 프로세스가 Root면 해커는 자동으로 관리자 권한을 가짐.
1-3) 자동화
Docker file
# [Stage 1] Build Stage: 빌드에 필요한 도구 설치 및 패키지 컴파일
FROM python:3.11-slim AS builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# 빌드 시에만 필요한 패키지 (psycopg2 등을 위해 필요할 수 있음)
RUN apt-get update && apt-get install --no-install-recommends -y \
gcc \
libpq-dev \
python3-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# 의존성 설치 (사용자 로컬 경로에 설치)
COPY requirements.txt .
RUN pip install --user --no-cache-dir pip==22.0.4 && \
pip install --user --no-cache-dir -r requirements.txt
builder stage는 기존과 동일
# [Stage 2] Final Stage: 실제 실행 환경 (Hardened)
FROM python:3.11-slim
WORKDIR /app
# 1. 보안: 비특권 사용자(pygoat) 생성
RUN groupadd -r pygoat && useradd -r -g pygoat pygoat
# 2. Builder 스테이지에서 설치된 바이너리만 복사
COPY --from=builder /root/.local /home/pygoat/.local
COPY --chown=pygoat:pygoat . .
# 3. 환경 변수 설정 (실행 경로 포함)
ENV PATH=/home/pygoat/.local/bin:$PATH \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# 4. 보안: pygoat 사용자로 전환
USER pygoat
EXPOSE 8000
# 5. CMD 통합: 마이그레이션 후 서버 실행
# sh -c를 사용하여 두 명령어를 순차적으로 실행합니다.
CMD ["sh", "-c", "python3 manage.py migrate && gunicorn --bind 0.0.0.0:8000 --workers 3 pygoat.wsgi"]
1. 계정 및 그룹 생성 (RUN) -> RUN groupadd -r [A] && useradd -r -g [A] [B]
- -r (System Account): 해당 계정을 로그인 기능이 없는 보안용 시스템 계정으로 설정함.
- -g [A] (Group Assign): 생성할 사용자(B)를 방금 만든 그룹(A)에 소속시킴.
- [A] (Group Name): 권한 관리를 위한 그룹 이름 (예: app_group).
- [B] (User Name): 실제로 프로세스를 실행할 사용자 ID (예: app_user).
2. 소유권 강제 변경 (COPY) -> COPY --chown=[B]:[A] . .
- --chown=[B]:[A]: 복사하는 모든 파일의 소유자를 사용자(B)로, 그룹을 그룹(A)으로 즉시 변경함.
- 필요성: 이 과정이 없으면 파일은 root 소유가 되어, 나중에 비특권 사용자가 파일을 읽지 못하는 보안/기능 오류가 발생함.
3. 실행 주체 전환 (USER) -> USER [B]
- USER [B]: 이후의 모든 명령(서버 실행 등)을 사용자(B)의 권한으로 수행하도록 고정함.
- 보안 효과: 공격자가 서버를 해킹해도 root가 아닌 제한된 사용자(B) 권한만 갖게 되어 시스템 전체 장악이 불가능함.
workflow.yml
# 5. Trivy: 빌드된 이미지 내 OS 패키지 및 라이브러리 취약점 진단
- name: Run Trivy Image Scan (Report Only)
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ secrets.DOCKERHUB_USERNAME }}/pygoat:hardened'
format: 'table'
exit-code: '0' # 취약점이 발견되어도 리포트 생성 후 빌드 지속 (사용자 요청 반영)
severity: 'CRITICAL,HIGH' # 고위험군 취약점 집중 출력
# 리포트 저장을 위한 JSON 포맷 스캔 별도 실행
- name: Generate JSON Report for Artifact
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ secrets.DOCKERHUB_USERNAME }}/pygoat:hardened'
format: 'json'
output: 'trivy-report.json'
exit-code: '0'
# 6. Artifact 업로드: 파이프라인 종료 후 GitHub Actions 결과 탭에서 취약점 리포트 다운로드 가능
- name: Upload Trivy Scan Report
uses: actions/upload-artifact@v4
with:
name: trivy-vulnerability-report
path: trivy-report.json
- 타겟 확정: 하드닝 설정이 완료된 pygoat:hardened 이미지를 검사 대상으로 지정함.
- 가용성 보장 (exit-code: 0): 실습 단계이므로 고위험 취약점이 발견되어도 빌드 중단 없이 프로세스를 지속함.
- 이중화 진단: 터미널 확인용(table) 로그와 정밀 분석용(json) 파일을 동시에 생성함.
- 증적 보관 (Artifact): 생성된 JSON 리포트를 업로드하여 사후 분석 및 사고 대응 자료로 보존함
1-4) 결과 분석
docker hardening 효과
- 패키지 수 87개 및 취약점 47개(기존 959개 대비 95% 감소) 달성 확인.
# 1. 전체 등급으로 재스캔 (필터링 없이)
trivy image -f json -o before_all.json [DOCKERHUB_ID]/pygoat:devsecops
trivy image -f json -o after_all.json [DOCKERHUB_ID]//pygoat:hardened
# 2. 전체 취약점 개수 비교
cat before_hardening.json | grep -i "VulnerabilityID" | wc -l
cat after_hardening.json | grep -i "VulnerabilityID" | wc -l
결과 959 -> 47
- 최적화 완료: 일반적인 Debian 이미지(450개)에서 Distroless로 전환하여 불필요한 쉘(Shell), 패키지 매니저, 유틸리티를 모두 제거한 상태
- 잔존 데이터 분석: 현재 남은 87개의 패키지와 47개의 취약점은 Python 인터프리터 자체와 필수 공유 라이브러리(glibc 등)에서 기인한 것이며, 더 이상 줄이는 것은 애플리케이션 구동에 영향을 줄 수 있음
- 런타임 취약점: OS 레벨 하드닝은 끝났으나, Python 애플리케이션 코드 자체의 취약점이나 종속 라이브러리(SCA) 이슈는 별도의 가드레일(Snyk, OWASP ZAP)로 대응해야 함.
결과 분석
severity: critical 에 해당하는 취약점만 뽑아내서 json 형식으로 저장한 후 패키지명, CVE ID 등 핵심 내용만 뽑아보면
# 1. 하드닝 전 이미지
trivy image --severity CRITICAL --quiet -f json 유저명/이전이미지:태그 > before_hardening_critical
# 파일에서 패키지명, CVE ID, 현재 버전을 요약해서 출력
cat before_hardening_critical | grep -E '"PkgName"|"VulnerabilityID"|"InstalledVersion"' | paste - - - | awk -F'"' '{print "Package: " $4 " | ID: " $8 " | Installed: " $12}'
기존의 이미지는 curl, git, libc6 등 시스템 라이브러리 및 유틸리티가 남아있지만

이후 이미지는 Django, PyYaML, Pillow 등 애플리케이션 실행에 필수적인 Python 패키지만 남은걸 알 수 있다.

이전 Snyk를 활용한 SCA 취약점 제거를 완벽하게 하지 않아서 남아있는 것으로 마찬가지로 두고 넘어가자.
2. Iac Security
IaC(Infrastructure as Code)는 '코드로 작성된 인프라' 라는 뜻으로 서버를 구축할 때 사람이 직접 터미널에 접속해 명령어를 입력하거나 UI를 클릭하는것이 아닌, 코드에 이런 사양의 서버와 네트워크를 만들어줘 라고 적어두고 도구가 이를 실행하게 한다.
Docker file도 '컨테이너'라는 가상화된 인프라 환경을 어떻게 구성할지 코드로 기술한 문서로 IaC에 해당한다.

IaC는 버전 관리와 빠른 배포가 가능하지만 설계도 내 보안 결함이 대규모로 확산될 위험이 공존한다. 그렇기에 이미지 하드닝 후, 인프라 정의서인 Docker file 및 Terraform 설정 내 루트 권한 방치나 자원 제한 누락 등의 결함을 점검하는 과정이 필수적이다. 이번 단계에서는 정적 분석 도구인 Checkov를 도입하여 보안 정책 위반 사항을 탐지할 예정이다.
2-1) Terraform 코드 작성
1) Terraform 핵심 개념
Terraform은 인프라를 수동으로 설정하지 않고 HCL(HashiCorp Configuration Language) 이라는 선언적 언어로 정의하며 클라우드 자원을 자동으로 생성,관리하는 IaC도구로 총 4단계 프로세스에 따라 작성된다.
- Provider 설정: AWS, Azure 등 주문을 넣을 대상 클라우드를 지정
- Resource 정의: HCL 문법을 사용하여 서버(EC2), 데이터베이스(RDS), 보안 그룹(SG) 등 필요한 자원을 코딩
- Plan(검토): terraform plan 명령으로 코드가 실행되었을 때 실제 인프라에 어떤 변화가 생길지 미리 시뮬레이션
- Apply(실행): 보안 검토가 완료된 코드를 terraform apply로 실행하여 실제 클라우드 환경에 인프라를 투사
2) Terraform 실습 예시 (main.tf)
취약하게 만든 Terraform 코드를 작성하여 이후 Checkov로 검사할 예정
# 1. Provider 정의: AWS를 사용하겠다고 선언
provider "aws" {
region = "ap-northeast-2"
}
# 2. Resource 정의: 보안 그룹(방화벽) 설정
resource "aws_security_group" "pygoat_sg" {
name = "pygoat-weak-sg"
description = "Example of insecure security group"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
# [취약점] 전 세계 어디서든 SSH 접속이 가능함 (0.0.0.0/0)
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
# 3. Resource 정의: 서버(EC2) 설정
resource "aws_instance" "pygoat_server" {
ami = "ami-0c55b159cbfafe1f0" # 예시용 이미지 ID
instance_type = "t2.micro"
# 위에서 만든 취약한 보안 그룹을 연결
vpc_security_group_ids = [aws_security_group.pygoat_sg.id]
tags = {
Name = "PyGoat-DevSecOps-Test"
}
}
2-2) Checkov란?
Checkov란 IaC의 정적 분석을 통해 보안 취약점과 설정 오류를 빌드 전 단계에서 탐지하는 오픈 소스 보안 도구이다.
작동 원리 및 특징
- 프레임워크 자동 인식: 파일 확장자와 내부 구조를 분석하여 Docker, Terraform, Kubernetes, Helm, CloudFormation 등을 자동으로 구분
- 정책 기반 스캔(Policy as Code): 수백 개의 기본 보안 정책(Check ID)을 내장하고 있으며, Python이나 YAML을 이용해 커스텀 정책 생성이 가능함.
- 멀티 플랫폼 지원: AWS, Azure, GCP 등 주요 클라우드 서비스 제공업체(CSP)의 모범 사례(Best Practices)를 준수하는지 점검함.
Check ID 체계 및 주요 항목 요약



2-3) Dockerfile 검사
pip3 install checkov
checkov -f Dockerfile
보안 정책(Check ID)의 각 항목에 대해서 p/f 여부를 확인할 수 있다.

취약점 해결
로그에서 확인된 CKV_DOCKER_2 취약점은 Dockerfile내에 컨테이너의 상태를 주기적으로 확인하는 HEALTHCHECK 명령어가 없기 때문에 발생
USER pygoat
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python3 -c 'import urllib.request; urllib.request.urlopen("http://localhost:8000/")' || exit 1
EXPOSE 8000
python 내부 라이브러리를 활용하여 Healthcheck 기능을 넣어서 다시 검사하면 pass 상태로 변경된다.

2-4) Terraform 파일 검사
Dockerfile과 마찬가지로 보안 정책(CheckID)에 반하는 취약점들이 나타나는데 그중 몇가지를 확인해보자
# 현재 디렉토리의 모든 Terraform 파일 검사
checkov -d . --framework terraform
1. 네트워크 노출 (CKV_AWS_24, CKV_AWS_107)
- 진단: SSH(22) 및 HTTP(80) 포트가 0.0.0.0/0으로 완전 개방됨.
- 위협: 무차별 대입 공격(Brute-force) 및 알려진 취약점을 이용한 서버 탈취 위험 가속화.
2. 데이터 보호 및 가시성 (CKV_AWS_8, CKV_AWS_126)
- 진단: EBS 볼륨 암호화 미적용 및 세부 모니터링(Detailed Monitoring) 비활성.
- 위협: 물리적 데이터 유출 시 평문 노출 위험 및 사고 발생 시 실시간 대응 지연.
3. 자격 증명 보안 (CKV_AWS_79, CKV2_AWS_41)
- 진단: IMDSv1 사용 중 및 IAM Role 미부착.
- 위협: SSRF(Server-Side Request Forgery) 공격을 통한 인스턴스 메타데이터 및 자격 증명 탈취 가능성 상존.
취약점 해결
1) Provider 정의 및 공급망 보안
# 1. Provider 및 버전 고정
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # 공급망 보안을 위한 버전 고정
}
}
}
provider "aws" {
region = "ap-northeast-2" # 서울 리전
}
terraform {} 블록: 테라폼이 실행되기 위한 최소한의 조건(프로바이더, 테라폼 버전 등)을 명시하는 메타데이터 블록( 인프라 요구사항 선언 블록)
- 보안적 의미 (공급망 보안):
- 소스 검증(source): hashicorp/aws와 같이 검증된 공식 출처를 명시하여 악성 코드가 삽입된 가짜 프로바이더 사용을 차단
- 버전 제어(version): ~> 5.0과 같은 제약 조건을 통해 업데이트로 인한 인프라 파괴(Breaking Changes)를 방지하고 일관된 배포 환경을 유지
provider "aws" {} 블록: 테라폼이라는 '설계자'가 실제 클라우드에서 사용하는 전용 번역기(플러그인)를 설정하는 블록 (서비스 연결 설정)
- 주요 설정: region = "ap-northeast-2"와 같이 리소스가 생성될 물리적 위치(서울 리전)를 결정합니다.
- 동작 원리: 이 블록이 선언되면 테라폼은 시스템 환경(환경 변수, IAM 역할 등)에서 AWS에 접속할 수 있는 자격 증명(열쇠)을 자동으로 찾아 통신을 시도한다.
2) IAM 역할 및 프로파일 설정 (신규 추가)
# 2. IAM 역할 및 프로파일 설정 (신분증 체계 구축)
resource "aws_iam_role" "pygoat_role" {
name = "pygoat-instance-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
}]
})
}
IAM(Identity and Access Management)란 AWS 리소스에 대한 인증과 인가를 제어하는 서비스이다.
Terraform에서 리소스 타입은 보통 [프로바이더]_[서비스]_[리소스] 형식으로 구성된다.

- aws_iam_role (고정 문법):
- 정의: AWS 프로바이더 내부에 정의된 고유 식별자
- 역할: "이 리소스는 AWS의 IAM 서비스 중 '역할(Role)'을 만드는 도구다"라고 테라폼에게 알려주는 공식 명칭으로 테라폼에서는 내부적으로 이 키워드를 만나면 AWS의 CreateRole 이라는 실제 API 호출 명령과 매핑한다. 오타가 나거나(aws_iam_roles 등), 임의로 변경하면 테라폼은 해당 리소스가 무엇인지 인식하지 못하고 에러를 발생시킨다.
- pygoat_role (자유 명칭) : 사용자가 임의로 붙이는 논리적 이름(식별자)로 중복되지 않게 지으면 된다.
- name: AWS 콘솔 화면에 실제로 표시될 리소스의 실명
- assume_role_policy(신뢰 관계) : 이 역할을 사용할 수 있는 주체(Principal)가 누구인가를 정의한다.
- Principal = { Service = "ec2.amazonaws.com" }: 오직 AWS의 EC2 서비스만이 이 역할을 사용할 수 있음을 나타낸다. 만약 해커가 다른 서비스(ex: Lambda)를 통해 가로채려해도 이 규칙에 의해 거부된다.
- Action = "sts:AssumeRole": 이 역할을 빌려 쓰는 행위를 허용함.
resource "aws_iam_instance_profile" "pygoat_profile" {
name = "pygoat-instance-profile"
role = aws_iam_role.pygoat_role.name
}
AWS에서는 보안상의 이유로 EC2 인스턴스가 IAM Role을 직접 읽을 수 없어서 인스턴스 프로파일 이라는 중간 매개체를 사용한다.
- resource "aws_iam_instance_profile": "인스턴스 프로파일"이라는 규격의 리소스를 생성하겠다는 선언.
- pygoat_profile: 테라폼 코드 내부에서 이 리소스를 가리키는 별명.
- name: AWS 콘솔에서 확인하게 될 실제 프로파일의 이름.
- role: 이 케이스 안에 어떤 신분증을 넣을 것인지 지정. (여기서는 앞서 만든 pygoat_role의 이름을 참조함)
3) 보안 그룹 및 규칙 최적화 (방화벽 강화)
# 3. 보안 그룹 본체 (설정 결함 제거)
resource "aws_security_group" "pygoat_sg" {
name = "pygoat-final-hardened-sg"
description = "Hardened security group for PyGoat DevSecOps" # CKV_AWS_23 해결
}
# 3. 보안 그룹 규칙 분리 (CKV_AWS_382 해결)
# SSH 접속 규칙
resource "aws_security_group_rule" "ssh_ingress" {
description = "Allow SSH from trusted admin IP only"
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["203.0.113.10/32"] # CKV_AWS_24 해결 (특정 IP 제한)
security_group_id = aws_security_group.pygoat_sg.id
}
# HTTP 접속 규칙
resource "aws_security_group_rule" "http_ingress" {
description = "Allow HTTP access from internal network"
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"] # CKV_AWS_107 해결 (내부망 제한)
security_group_id = aws_security_group.pygoat_sg.id
}
# 아웃바운드 규칙 (나가는 문)
resource "aws_security_group_rule" "all_egress" {
# checkov:skip=CKV_AWS_382: "Inline rules are preferred for single egress to prevent complexity in this lab"
# checkov:skip=CKV_AWS_23: "Description is already provided"
description = "Allow all outbound traffic for updates"
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.pygoat_sg.id
}
1) 리소스 구성 요소: 본체와 규칙의 분리
- 보안 그룹 본체 (aws_security_group):
- 보안 규칙들을 담는 '빈 방(Container)' 역할.
- 이름(name)과 설명(description)을 통해 리소스의 정체성 정의.
- 보안 규칙 (aws_security_group_rule):
- 실제 트래픽을 제어하는 '개별 정책(Policy)' 역할.
- ingress(들어오는 문)와 egress(나가는 문)로 구분하여 설정.
2) 핵심 연결 고리: (security_group_id)
본체와 규칙은 테라폼의 참조(Referencing) 메커니즘을 통해 물리적으로 결합된다.
- 연결 방식: 규칙 리소스 내부의 security_group_id 인자에 본체 리소스의 ID 값(aws_security_group.pygoat_sg.id)을 할당.
- 논리적 흐름:
- 테라폼이 본체(pygoat_sg)를 생성하고 AWS로부터 고유 ID를 발급받음.
- 이후 생성되는 규칙들이 해당 ID를 '소속 주소'로 인식하고 본체에 귀속됨.
4) EC2 인스턴스 시스템 경화
# 4. 서버(EC2) 설정 (시스템 경화)
resource "aws_instance" "pygoat_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro" # EBS 최적화를 지원하는 타입으로 변경
ebs_optimized = true # [추가] EBS 최적화 명시적 활성화 (CKV_AWS_135 해결)
monitoring = true # [추가] 세부 모니터링 활성화 (CKV_AWS_126 해결)
# 신분증(IAM Role) 연결 (CKV2_AWS_41 해결)
iam_instance_profile = aws_iam_instance_profile.pygoat_profile.name
# 위에서 만든 강화된 보안 그룹 연결
vpc_security_group_ids = [aws_security_group.pygoat_sg.id]
# [추가] 루트 볼륨 암호화 설정 (CKV_AWS_8 해결)
root_block_device {
encrypted = true
}
# [추가] IMDSv2 사용 강제 (CKV_AWS_79 해결)
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # 토큰 사용을 필수화하여 세션 탈취 방어
}
tags = {
Name = "PyGoat-Hardened-Server"
}
}
1) 인프라 보안 및 성능 최적화
- 리소스 최적화 (CKV_AWS_135): ebs_optimized = true 설정을 통해 인스턴스와 스토리지(EBS) 간 전용 대역폭 확보 및 성능 병목 현상 방지.
- 가시성 확보 (CKV_AWS_126): monitoring = true 설정을 통해 1분 단위의 세부 모니터링 활성화 및 이상 징후 조기 탐지 체계 구축.
- 데이터 보호 (CKV_AWS_8): root_block_device 내 encrypted = true 설정을 통해 물리적 디스크 탈취 시 데이터 유출 원천 차단.
2) 신원 및 접근 제어 결합 (통합 보안)
- 권한 주입 (CKV2_AWS_41): iam_instance_profile을 통해 앞서 생성한 신분증 케이스를 서버에 전달. 서버가 비밀번호 없이 안전하게 AWS 자원을 이용하도록 설정.
- 네트워크 방어: vpc_security_group_ids에 앞서 하드닝한 보안 그룹 ID를 연결하여 허가된 IP와 포트만 통과시키는 가드레일 적용.
3) 지능형 공격 방어 (IMDSv2)
- 세션 탈취 방어 (CKV_AWS_79): metadata_options에서 http_tokens = "required"를 설정하여 최신 메타데이터 서비스(IMDSv2) 강제 적용.
- SSRF 공격 차단: 단순 HTTP 요청이 아닌 세션 토큰 기반 인증 방식을 사용하여, 공격자가 서버 내부 메타데이터(IAM 자격 증명 등)를 탈취하는 행위 차단.
최종 흐름
테라폼 설계도에 따라 AWS에 'EC2 전용 신분증(Role)'을 만들고 이를 안전하게 전달할 '케이스(Profile)'를 준비.
동시에 서버의 출입문을 통제할 '강화된 보안 그룹(Security Group)'을 생성하여 허가된 관리자 IP와 내부망만 접속 허가
이제 EC2 서버를 생성하면서 앞서 만든 신분증과 보안 그룹을 연결하는데, 이때 서버 내부적으로도 데이터 암호화(EBS Encryption)와 세부 모니터링, 그리고 신분증 탈취를 막는 강화된 인증 체계(IMDSv2)를 적용하여 시스템 자체를 단단하게 경화
2-5) IaC Security 자동화
이미지 빌드 하기 전 단계에 checkov 과정 추가
# 파이프라인 명칭: DevSecOps 1~2주차 통합 가드레일
# 주요 목적: 시크릿 탐지, SAST, SCA, 이미지 하드닝, 취약점 리포팅, 이미지 서명 자동화
name: DevSecOps 1-Week Automation Pipeline
permissions:
contents: read # 코드 스캔을 위해 리포지토리 콘텐츠 읽기 권한 필요
id-token: write # SonarCloud 인증을 위한 OIDC 토큰 발급 권한
pull-requests: write # PR에 보안 분석 결과 코멘트 작성 권한 (옵션)c
security-events: write # checkov나 trivy 결과를 github security tab에 업로드
on:
push:
branches: [ "main" ] # main 브랜치에 코드 푸시 시 자동 실행
pull_request:
branches: [ "main" ] # PR 생성 시 보안 검사 강제화
jobs:
# [Job 1] 코드 및 의존성 보안 스캔 (Static Analysis)
security-scan:
name: Security & Quality Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0 # 모든 커밋 히스토리를 가져와 Gitleaks가 전체 이력을 스캔할 수 있도록 설정
# 1. Gitleaks: 커밋 히스토리 내 평문 시크릿(API Key, Password 등) 탐지
- name: Gitleaks Scan
run: |
# exit-code=2: 시크릿 탐지 시 빌드를 즉시 중단하여 유출 방지
docker run -v ${PWD}:/path zricethezav/gitleaks:latest detect --source="/path" -v --exit-code=2
# 2. SonarQube: 정적 코드 분석을 통해 취약한 코드 패턴(XSS, SQLi 등) 탐지
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }}
-Dsonar.organization=${{ secrets.SONAR_ORG_KEY }}
-Dsonar.qualitygate.wait=false # 분석 완료를 기다리지 않고 비동기로 진행
# SonarQube 분석 결과를 바탕으로 품질 게이트 통과 여부 확인 (사용자 정의 스크립트)
- name: Custom Security Gate Check
run: python check_gate.py "${{ secrets.SONAR_PROJECT_KEY }}" "${{ secrets.SONAR_TOKEN }}"
# 3. Snyk: 오픈소스 라이브러리(SCA) 내 알려진 취약점(CVE) 스캔
- name: Install dependencies & Snyk
run: |
sudo apt-get update && sudo apt-get install -y libjpeg-dev zlib1g-dev # Pillow 등 컴파일에 필요한 의존성
python -m pip install --upgrade pip
pip install -r requirements.txt
npm install -g snyk # Snyk CLI 설치
- name: Run Snyk Test
continue-on-error: true # 학습용 PyGoat은 취약점이 많으므로 중단 없이 결과만 확인
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
# requirements.txt 기반으로 라이브러리 보안성 검사
snyk test --file=requirements.txt --org=${{ secrets.SNYK_ORG_ID }} --skip-unresolved=true
# 4. Checkov: Infra(Terraform) 및 Container(Dockerfile) 보안 스캔
- name: Run Checkov Scan (Full IaC Scrutiny)
id: checkov
uses: bridgecrewio/checkov-action@v12.2983.0 # v3 계열의 안정된 버전 사용 (공급망 보안 버전 고정 원칙)
with:
directory: . # 프로젝트 루트(.) 내의 모든 테라폼 파일과 Dockerfile을 스캔 대상으로 지정
skip_path: dockerized_labs # 실습 예제 제외
soft_fail: false # false: 보안 결함(Failed) 발견 시 Exit Code 1을 발생시켜 파이프라인 즉시 중단
framework: all # 테라폼과 Docker를 포함한 모든 지원 프레임워크 자동 탐지
output_format: cli # 보안 분석 결과를 CLI 형태로 상세 출력
# [Job 2] 이미지 빌드, 하드닝 검증 및 공급망 보안 (Infrastructure & Supply Chain)
build-and-hardening:
name: Docker Hardening & Supply Chain Security
needs: security-scan # Job 1이 성공해야만 이미지 빌드 시작
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
# 5. Docker 이미지 빌드 (Hardened Tag 적용)
# Dockerfile의 멀티 스테이지 빌드와 비특권 사용자 설정이 반영된 이미지 생성
- name: Build Docker Image
run: |
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/pygoat:hardened .
# 6. Trivy: 빌드된 이미지 내 OS 패키지 및 라이브러리 취약점 진단
- name: Run Trivy Image Scan (Report Only)
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ secrets.DOCKERHUB_USERNAME }}/pygoat:hardened'
format: 'table'
exit-code: '0' # 취약점이 발견되어도 리포트 생성 후 빌드 지속 (사용자 요청 반영)
severity: 'CRITICAL,HIGH' # 고위험군 취약점 집중 출력
# 리포트 저장을 위한 JSON 포맷 스캔 별도 실행
- name: Generate JSON Report for Artifact
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ secrets.DOCKERHUB_USERNAME }}/pygoat:hardened'
format: 'json'
output: 'trivy-report.json'
exit-code: '0'
# 7. Artifact 업로드: 파이프라인 종료 후 GitHub Actions 결과 탭에서 취약점 리포트 다운로드 가능
- name: Upload Trivy Scan Report
uses: actions/upload-artifact@v4
with:
name: trivy-vulnerability-report
path: trivy-report.json
# 8. Docker Hub 로그인 및 푸시
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push Docker Image
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/pygoat:hardened
# 9. Syft: 소프트웨어 자재 명세서(SBOM) 생성 (공급망 보안 가시성 확보)
- name: Generate SBOM (Syft)
uses: anchore/sbom-action@v0
with:
image: ${{ secrets.DOCKERHUB_USERNAME }}/pygoat:hardened
format: 'spdx-json'
output-file: 'sbom.spdx.json'
# 10. Cosign: 이미지 무결성 증명을 위한 전자 서명 및 SBOM 어테스테이션
- name: Install Cosign
uses: sigstore/cosign-installer@v3.5.0
- name: Sign & Attest Image
run: |
# 환경 변수의 개인키를 파일로 생성
echo "$COSIGN_KEY" > cosign.key
# 이미지 서명: 배포된 이미지가 변조되지 않았음을 증명
cosign sign --key cosign.key -y ${{ secrets.DOCKERHUB_USERNAME }}/pygoat:hardened
# SBOM 어테스테이션: 빌드 당시의 의존성 정보를 이미지에 영구 결합 (VEX 등에 활용)
cosign attest --key cosign.key --type spdxjson --predicate sbom.spdx.json -y ${{ secrets.DOCKERHUB_USERNAME }}/pygoat:hardened
env:
COSIGN_KEY: ${{ secrets.COSIGN_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
3. 제로 트러스트 기밀 관리 (Vault Secret)
HashiCorp Vault는 동적 인프라 환경에서 비밀번호, 인증 토큰, API 키 등 민감한 정보를 안전하게 관리하기 위한 중앙 집중형 기밀 관리 솔루션이다.
모든 기밀 데이터는 암호화되어 저장되며 Zero Trust 원칙에 따라 매 요청마다 신원을 확인한다.
Github Secret 대신 Vault를 사용하는 이유는 다음과 같다.
- 기밀 파편화 방지: 코드(settings.py), CI/CD(GitHub Secrets), 서버 환경 변수 등에 흩어진 시크릿을 한곳에서 통합 관리
- 동적 시크릿 (Dynamic Secrets): 고정된 비밀번호가 아니라, 요청 시점에만 유효한 임시 자격 증명을 생성하여 유출 시 피해를 최소화
- 강력한 감사 로그 (Audit Log): "누가, 언제, 어떤 시크릿을 조회했는지" 모든 이력을 기록하여 보안 사고 발생 시 추적성을 확보
- 코드형 보안 (Security as Code): 시크릿 접근 권한을 정책(Policy) 파일로 정의하여 자동화된 가드레일 구축 가능
실습 흐름
1단계: Vault 서버 구축 및 초기화 (Infrastructure)
- Vault 기동: Docker를 활용하여 로컬 환경에 Vault 컨테이너 실행.
- 봉인 해제(Unseal): 금고를 열기 위한 마스터 키 생성 및 초기 설정.
- 저장소 활성화: 데이터를 저장할 공간인 KV(Key-Value) 엔진 활성화.
2단계: 시크릿 저장 및 정책 설정 (Data & Policy)
- 데이터 이관: settings.py의 SECRET_KEY와 기존 GitHub Secrets 값들을 Vault 내부 경로(secret/pygoat/)에 저장.
- 권한 정의(Policy): 특정 앱(PyGoat)이나 파이프라인(GitHub Actions)이 본인의 값만 읽을 수 있도록 세부 정책 작성.
3단계: 애플리케이션 코드 살균 (Refactoring)
- 평문 제거: pygoat/settings.py 내부에 하드코딩된 모든 문자열 시크릿 삭제.
- 환경 변수 전환: os.environ.get()을 사용하여 실행 시점에 외부에서 값을 주입받는 구조로 코드 수정.
4단계: GitHub Actions 연동 및 검증 (Automation)
- OIDC 인증: GitHub와 Vault 간의 신뢰 관계를 구축하여 ID/PW 없이 인증 수행.
- 런타임 주입: 파이프라인 실행 시 Vault에서 값을 실시간으로 가져와 PyGoat에 전달.
- 보안 검증: Gitleaks 스캔을 통해 코드 내 시크릿 노출 0건(Zero) 확인 및 정상 작동 테스트.
3-1) Vault 서버 구축 및 초기화
1. Docker를 이용한 Vault 서버 기동
docker run -d --name vault-server \
-p 8200:8200 \
--cap-add=IPC_LOCK \
--cap-add=SETFCAP \
--security-opt seccomp=unconfined \
-e 'VAULT_LOCAL_CONFIG={
"storage": {"file": {"path": "/vault/file"}},
"listener": {"tcp": {"address": "0.0.0.0:8200", "tls_disable": 0}},
"disable_mlock": true
}' \
hashicorp/vault:1.15.0 server
1) -d --name vault-server
- 의미: 컨테이너를 백그라운드(detach)에서 실행하며, 이름을 vault-server로 지정함.
- 용도: 컨테이너 관리(중지, 삭제, 로그 확인)를 용이하게 함.
2) --cap-add=IPC_LOCK
- 의미: 컨테이너에 IPC_LOCK이라는 리눅스 커널 권한을 부여함.
- 보안적 이유: Vault는 시크릿을 메모리에 저장하는데, 시스템이 메모리 부족 시 이 데이터를 디스크(Swap 영역)로 옮기는 것을 방지함. 시크릿이 암호화되지 않은 채 디스크에 남는 것을 막는 메모리 하드닝 설정임.
3) --cap-add=SETFCAP
- 기술적 이유: Vault 바이너리는 실행 시 파일 시스템에 성능 권한(Capabilities)을 직접 설정하려고 시도
- 해결 내용: 이 권한이 없으면 Operation not permitted 에러와 함께 기동 자체가 거부된다.
4) --security-opt seccomp=unconfined
- Seccomp(Secure Computing mode)의 의미: 컨테이너가 리눅스 커널에 요청하는 시스템 호출(Syscall)을 필터링하는 보안 가드레일임. 허용되지 않은 Syscall(예: 파일 삭제, 커널 모듈 조작 등)을 원천 차단하여 컨테이너 탈출 공격을 방지함.
- unconfined의 의미: 단어 그대로 "제한 없음"을 뜻하며, 도커의 기본 Seccomp 보안 프로필(필터)을 적용하지 않겠다는 설정임.
- 사용 이유 (정찰 모드): Vault와 같은 고성능 Go 애플리케이션은 기본 필터에 막히는 특수한 Syscall을 빈번하게 사용함. 초기 구축 시 모든 제약을 풀어(unconfined) 서비스 가용성을 먼저 확보하고, strace를 통해 필수 Syscall 목록을 수집하기 위한 전제 조건임
5) -e 'VAULT_LOCAL_CONFIG=...'
가장 중요한 설정부로, Vault의 동작 방식을 JSON 형태로 정의함.
- "storage": {"file": {"path": "/vault/file"}} : dev 모드와 달리 데이터를 메모리가 아닌 컨테이너 내부의 /vault/file 경로에 저장함. 컨테이너가 재시작되어도 데이터가 유지됨.
- "listener": {"tcp": {"address": "0.0.0.0:8200", "tls_disable": 0}}
- 접속 설정: 모든 네트워크 인터페이스(0.0.0.0)의 8200 포트로 접속을 허용함.
- tls_disable": 0: HTTPS(TLS) 통신
- disable_mlock: true
- 진단: IPC_LOCK 권한을 줬음에도 WSL2 커널 환경에 따라 실제 메모리 잠금이 실패할 수 있다.
- 대응: 이때 Vault가 에러를 뿜으며 죽지 않도록, "잠금 시도는 하되 실패해도 기동은 해라"라는 역할
6) hashicorp/vault:1.15.0 server
- 명령어: 기본 실행 모드를 dev가 아닌 server로 명시함.
- 결과: 이로 인해 Vault는 실행 직후 봉인(Sealed) 상태가 되며, 앞서 설명한 init 및 unseal 과정을 거쳐야만 사용할 수 있게 됨.
2) HTTPS 통신 환경 구축
HTTPS 통신환경을 만들어야 하는데 인증서가 없으니까 서버를 실행할 수 없기 때문에 임시 인증서를 만들어서 서버를 실행한 후 PKI 엔진을 이용해 인증서 동적 발급을 해볼 예정
먼저 wsl 임시 인증서 생성
openssl req -x509 -newkey rsa:2048 -keyout vault.key -out vault.crt -days 365 -nodes \
-subj "/CN=127.0.0.1" \
-addext "subjectAltName = IP:127.0.0.1"
디렉토리에 vault.crt(인증서)와 vault.key(개인키) 생성

읽기 권한 부여
vault 공식 이미지는 보안을 위해 root가 아닌 vault라는 별도 유저로 실행되므로 읽기 권한 부여
chmod 644 vault.crt vault.key
서버 실행
docker run -d --name vault-server \
-p 8200:8200 \
--cap-add=IPC_LOCK \
--cap-add=SETFCAP \
--security-opt seccomp=unconfined \
-v $(pwd)/vault.crt:/vault/config/vault.crt \
-v $(pwd)/vault.key:/vault/config/vault.key \
-e "VAULT_LOCAL_CONFIG={
\"storage\": {\"file\": {\"path\": \"/vault/file\"}},
\"listener\": {
\"tcp\": {
\"address\": \"0.0.0.0:8200\",
\"tls_disable\": 0,
\"tls_cert_file\": \"/vault/config/vault.crt\",
\"tls_key_file\": \"/vault/config/vault.key\"
}
},
\"disable_mlock\": true
}" \
hashicorp/vault:1.15.0 server
-v : wsl의 파일을 컨테이너 내부 /vault/config/vault.crt 경로로 투영
config 내의 tls_cert_file : 내부적으로 load할때 읽을 경로
환경 변수 설정
# 1. 컨테이너 접속
docker exec -it vault-server sh
# 2. 내부에서 파일 존재 확인 (반드시 확인!)
ls -l /vault/config/vault.crt
# 3. 환경변수 설정 (작업 세션에 즉시 반영)
export VAULT_ADDR='https://127.0.0.1:8200'
export VAULT_CACERT='/vault/config/vault.crt'
# 4. 상태 확인
vault status
3. Unseal
# 컨테이너 내부에서 실행 중이라면
vault operator init
# 3번 반복 실행 (매번 다른 Unseal Key 입력)
vault operator unseal # [첫 번째 키 입력]
vault operator unseal # [두 번째 키 입력]
vault operator unseal # [세 번째 키 입력]
# Sealed: false가 되면 성공!
vault status
init을 하면 unseal key 5개와 Initial Root Token이 1개 생성되는데 메모장에 복사해두자.
총 3개를 아무거나 입력하여 잠금을푼다. sealed가 true가 되야함

4) Vault CLI 설치
# 1. 기존 설정 삭제 (충돌 방지)
sudo rm -f /etc/apt/sources.list.d/hashicorp.list
# 2. GPG 키 다운로드 및 권한 설정
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
sudo chmod 644 /usr/share/keyrings/hashicorp-archive-keyring.gpg
# 3. 저장소 등록 (Ubuntu 24.04 Noble 자동 인식)
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# 4. 패키지 업데이트 및 설치
sudo apt update && sudo apt install vault
# 5. 설치 확인
vault -version
wsl에서 서버(docker)를 인식할 수 있도록 환경변수도 설정
# 1. 서버 주소 설정 (Docker 포트 포워딩 기준)
export VAULT_ADDR='https://127.0.0.1:8200'
# 2. 임시 인증서 경로 설정 (현재 폴더에 vault.crt가 있는 경우)
# 경로가 다르다면 실제 파일 위치로 수정하세요.
export VAULT_CACERT="$(pwd)/vault.crt"
# 3. (선택) 인증서 검증 건너뛰기
# CACERT 설정이 번거로울 때 사용하지만, 보안 실습을 위해 가급적 CACERT를 사용하세요.
# export VAULT_SKIP_VERIFY=true
# 4. 연결 상태 최종 확인
vault status
설정 영구 반영하려면 bashrc 변경
# bashrc 파일 하단에 추가
echo "export VAULT_ADDR='https://127.0.0.1:8200'" >> ~/.bashrc
echo "export VAULT_CACERT='$(pwd)/vault.crt'" >> ~/.bashrc
# 변경사항 적용
source ~/.bashrc
5) PKI 엔진 설치
root token 로그인
vault login [사용자의_루트_토큰_입력]
pki 엔진 활성화 및 수명 설정
# PKI 엔진 활성화
vault secrets enable pki
# 최대 유효기간을 10년(87600시간)으로 설정
vault secrets tune -max-lease-ttl=87600h pki
자체 Root CA 인증서 생성
vault write -field=certificate pki/root/generate/internal \
common_name="DevSecOps-Internal-Root-CA" \
ttl=87600h > internal_root_ca.crt
- 자동 저장: pki/root/generate/internal 명령을 실행하는 순간, Vault는 개인키(Private Key)와 인증서를 생성하여 내부 저장소에 즉시 기록함.
- 보안 가드레일: 개인키가 일반 파일 시스템에 노출되지 않으므로, 관리자조차 개인키 파일을 탈취할 수 없는 제로 트러스트 보안 환경을 구축함.
인증서 복사본이 wsl 디렉토리에 저장됨
발급 규칙 정의
로컬 테스트를 위해 local 도메인 허용, 후에 필요한 도메인 추가 가능
# Role 규칙 업데이트: 모든 이름 허용 및 IP SAN 허용 추가
vault write pki/roles/my-local-services \
allow_any_name=true \
allow_ip_sans=true \
max_ttl="720h" \
ttl="720h"
인증서 발급
vault write -format=json pki/issue/my-local-services \
common_name="127.0.0.1" \
ip_sans="127.0.0.1" > cert_bundle.json
6) 저장소 (KV 엔진) 활성화
pki 엔진이 안전한 통로를 만드는 역할이라면 kv엔진은 그 통로를 통해 옮길 비밀번호 원본을 보관하는 장소이다.
# 위에서 복사한 Root Token으로 로그인
vault login [YOUR_ROOT_TOKEN]
# KV-v2 엔진 활성화 (경로: secret)
vault secrets enable -path=secret kv-v2
wsl2 환경에서 vault를 활용하여 HTTPS 통신 체계와 PKI 인증서 발급 환경 구축 전체 과정 정리
가장 먼저 통신의 보안 기초를 다지기 위해 WSL2 호스트에서 임시 TLS 인증서와 개인키를 생성했습니다. openssl을 이용해 127.0.0.1 주소에 대한 SAN(Subject Alternative Name) 설정을 포함한 인증서를 만들었으며, Docker 컨테이너 내의 vault 사용자가 이 파일을 읽을 수 있도록 파일 권한을 644로 조정하는 보안 설정을 수행했습니다.
그다음 단계로 Vault 서버를 Docker 컨테이너로 실행했습니다. 이때 앞서 생성한 호스트의 인증서 파일들을 컨테이너 내부의 설정 경로로 마운트하였고, 환경 변수(VAULT_LOCAL_CONFIG)를 통해 TLS 활성화 및 인증서 경로를 명시하여 서버가 HTTPS 모드로 작동하도록 구성했습니다. 서버 실행 후에는 컨테이너 내부에 접속하여 초기화(Initialize) 및 봉인 해제(Unseal) 작업을 진행했습니다. vault operator init으로 생성된 5개의 Unseal Key 중 3개를 입력하여 서버를 활성 상태(Sealed: false)로 전환했으며, 이 과정에서 발급된 Root Token은 향후 관리자 인증을 위해 안전하게 보관했습니다.
서버가 정상 가동된 후에는 WSL 호스트 환경에서도 Vault를 제어할 수 있도록 Vault CLI를 설치하고 환경 변수를 설정했습니다. VAULT_ADDR을 HTTPS 주소로 지정하고, VAULT_CACERT에 호스트 측 인증서 경로를 연결하여 CLI와 서버 간의 신뢰 관계를 형성했습니다. 이어서 관리자 로그인을 마친 뒤 PKI 시크릿 엔진을 활성화하고 인증서 유효기간을 10년으로 튜닝했습니다.
본격적인 인증서 체계 구축을 위해 Internal Root CA를 생성했습니다. 이 명령을 통해 Vault 내부 저장소에 개인키가 안전하게 생성되었으며, 이는 관리자조차 개인키 파일에 직접 접근할 수 없는 제로 트러스트 보안 모델을 충실히 따르는 방식입니다. 이후 실제 인증서 발급 조건을 규정하는 발급 규칙(Role)을 정의하여 로컬 도메인 및 IP에 대한 허용 범위를 설정했습니다.
마지막으로 정의된 Role을 통해 실제 엔드포인트 인증서를 발급받았습니다. write 명령을 수행하면 Vault가 내부에 보관 중인 Root CA의 권한으로 서명된 인증서 뭉치를 JSON 형태로 반환하며, 이로써 안전한 통신 통로를 확보하기 위한 PKI 구축 프로세스가 완료되었습니다. 이 과정은 단순히 도구를 사용하는 것을 넘어, 데이터 살균(CDR)이나 제로 트러스트(ZTA) 체계 구축을 위한 핵심적인 보안 가드레일 역할을 합니다.
3-2) secret 저장 및 정책 설정
1) github secret값 vault에 이전
# SonarQube 관련 설정
vault kv put secret/infra/sonarqube \
host_url="YOUR_SONAR_HOST_URL" \
token="YOUR_SONAR_TOKEN" \
org_key="YOUR_SONAR_ORG_KEY" \
project_key="YOUR_SONAR_PROJECT_KEY"
# Snyk 및 Docker 관련 설정
vault kv put secret/infra/common \
snyk_token="YOUR_SNYK_TOKEN" \
snyk_org_id="YOUR_SNYK_ORG_ID" \
docker_token="YOUR_DOCKER_TOKEN" \
dockerhub_username="YOUR_DOCKERHUB_USERNAME" \
dockerhub_token="YOUR_DOCKERHUB_TOKEN" \
gh_token="YOUR_GH_TOKEN"
# Cosign 이미지 서명 설정
vault kv put secret/security/cosign \
key="YOUR_COSIGN_KEY" \
password="YOUR_COSIGN_PASSWORD"
- 데이터 확인: vault kv get secret/infra/sonarqube 명령어로 데이터가 정확히 들어갔는지 확인 필수.
- 버전 관리: KV-v2를 사용 중이므로, 값을 잘못 입력해 다시 put 하면 자동으로 Version 2가 생성되어 이력이 관리됨.
- 보안 가드레일: 이제 GitHub Secrets에 남아있는 값들은 삭제하거나, Vault에서 값을 가져오는 AppRole/JWT 토큰 하나로 대체
2) 코드 내 하드코딩된 문자열 vault에 이전(ex. settings.py)

코드 내 문자열을 vault kv에 저장
# 큰따옴표를 사용하여 특수문자 오류 방지
vault kv put secret/pygoat/config \
django_secret_key="lr66%-a!$km5ed@n5ug!tya5bv!0(yqwa1tn!q%0%3m2nh%oml" \
sensitive_data="FLAGTHATNEEDSTOBEFOUND"
실제 코드에서는 환경변수로 vault의 값을 가져옴

3) 정책 설정
# data/ 경로를 명시한 정책 파일 생성
cat <<EOF > github-actions-policy.hcl
# 인프라 공용 시크릿 (Docker, GitHub 등)
path "secret/data/infra/*" {
capabilities = ["read"]
}
# PyGoat 앱 시크릿 (Django Key, DB PW 등)
path "secret/data/pygoat/*" {
capabilities = ["read"]
}
# 보안 도구 시크릿 (Snyk, SonarQube 등)
path "secret/data/security/*" {
capabilities = ["read"]
}
EOF
왜 정책(Policy)만 수동으로 data를 써야 하는가?
- 명령어 (CLI): 사용자의 편의를 위해 Vault 프로그램이 중간에 data를 자동으로 붙여서 서버에 요청을 보냄.
- 정책 (Policy): 보안의 핵심인 정책은 "가장 낮은 수준의 실제 경로"를 직접 통제해야 함. 따라서 자동 완성 기능 없이, 관리자가 실제 물리적 경로인 data/를 명시해야만 권한 검사(Validation)를 통과할 수 있음.
이후 정책 등록 및 리스트 확인
# 1. 정책 등록
vault policy write github-actions-policy github-actions-policy.hcl
# 2. 등록된 정책 리스트 확인
vault policy list
# 3. 정책 내용이 잘 들어갔는지 최종 확인
vault policy read github-actions-policy
AppRole 생성
- RoleID (아이디 역할): 특정 서비스(예: PyGoat)를 식별하는 값. 외부에 노출되어도 이 값만으로는 금고를 열 수 없음.
- SecretID (비밀번호 역할): 금고를 열기 위한 실제 열쇠. Vault 내부에서만 생성되며, 유출 시 즉시 폐기 가능.
- 신뢰의 결합: RoleID와 SecretID가 둘 다 있어야만 Vault가 임시 통행증(Token)을 발급해 줌.
핵심 이점
- 최소 권한 부여 (Least Privilege): * "사람" 계정은 관리자 권한을 가질 때가 많지만, github-actions-role은 오직 pygoat 시크릿만 읽을 수 있도록 정책으로 묶여 있음.
- 책임 추적 (Audit): * 로그를 확인했을 때 "누가" 시크릿을 가져갔는지 명확히 기록됨 (예: "AppRole: github-actions-role이 13:05에 시크릿 조회").
- 자동화 최적화: * GitHub Actions 같은 자동화 도구가 프로그래밍 방식으로 시크릿을 안전하게 호출하기 위해 설계된 표준 방식임.
approle 엔진 활성화 및 생성
# 1. AppRole 인증 엔진 활성화 (이미 활성화되어 있다면 무시됨)
vault auth enable approle
# 2. github-actions라는 이름의 Role 생성 및 정책 연결
# secret_id_num_uses=0 : SecretID 사용 횟수 무제한 (실습 편의성)
# token_ttl=1h : 발급된 토큰의 유효시간 1시간
vault write auth/approle/role/github-actions-role \
token_policies="github-actions-policy" \
token_ttl=1h \
token_max_ttl=4h
핵심 정보 추출
# 1. RoleID 확인 (아이디)
vault read auth/approle/role/github-actions-role/role-id
# 2. SecretID 생성 및 확인 (비밀번호 - 생성할 때마다 바뀜)
vault write -f auth/approle/role/github-actions-role/secret-id
github secret에 vault 정보 등록
- VAULT_ADDR: https://127.0.0.1:8200 (우리가 만든 Vault 주소)
- VAULT_ROLE_ID: 아까 복사한 RoleID
- VAULT_SECRET_ID: 아까 복사한 SecretID
- VAULT_SKIP_VERIFY: true (로컬 사설 인증서 사용 시 검증 건너뛰기 설정용)
3-3) Self-hosted-runner
GitHub Actions의 작업(Job)을 GitHub가 제공하는 공용 서버가 아닌, 사용자가 직접 관리하는 서버(내 WSL2)에서 실행하는 방식
사용 이유
- 로컬 네트워크 접근 (Private Access):
- GitHub 서버는 내 컴퓨터의 127.0.0.1:8200에 접속할 수 없음.
- 내 컴퓨터 안에서 도는 Runner는 로컬 Vault에 자유롭게 접속하여 시크릿을 가져올 수 있음.
- 보안성 (No Inbound):
- 외부에서 내 컴퓨터로 들어오는 문(포트)을 열 필요가 없음.
- Runner가 GitHub에 접속해서 "할 일 있니?"라고 물어보고 가져오는 방식(Outbound)이라 안전함.
- 비용 및 자원 최적화 : 이미 구축된 WSL2의 도구들(Docker, Python, Vault CLI)을 별도 설치 없이 바로 활용 가능함.
설치 방법
GitHub 저장소 설정 (웹)
- 저장소 상단 Settings 클릭.
- 왼쪽 메뉴에서 Actions → Runners 선택.
- New self-hosted runner 버튼 클릭.
- Runner 이미지 선택: Linux / 아키텍처: x64.
후 화면에 나오는 명령어 순서대로 복사해서 실행
예시)
# 폴더 생성 및 이동
mkdir actions-runner && cd actions-runner
# 설치 파일 다운로드 (화면의 curl 명령어 복사)
curl -o actions-runner-linux-x64-2.xxx.x.tar.gz -L https://github.com/...
# 압축 해제
tar xzf ./actions-runner-linux-x64-2.xxx.x.tar.gz
# Runner 등록 (화면의 ./config.sh 명령어 복사)
./config.sh --url https://github.com/사용자ID/저장소명 --token 복사한토큰
# 실행 (일꾼 가동 시작!)
./run.sh
run.sh는 터미널을 끄면 같이 종료되므로 아래는 서비스로 등록하여 백그라운드에서 돌게만드는방법
# 1. 서비스 설치 (관리자 권한 필요)
sudo ./svc.sh install
# 2. 서비스 시작
sudo ./svc.sh start
# 3. 상태 확인
sudo ./svc.sh status
도커 소켓 권한 부여
sudo usermod -aG docker $USER
sudo chown taemin:docker /var/run/docker.sock
sudo chmod 660 /var/run/docker.sock
3-4) 자동화
| 구분 | 기존 (1주차) | 변경 (통합 가드레일) | 비고 |
| 비밀번호 관리 | GitHub Secrets 직접 참조 | HashiCorp Vault 연동 및 동적 주입 | 보안성 강화 (ZTA) |
| 실행 환경 | ubuntu-latest (공용) | self-hosted (자체 호스팅) | 인프라 통제권 확보 |
| 작업 구조 | 2개 Job (Scan → Build) | 1개 통합 Job (pipeline) | 오버헤드 감소 및 속도 향상 |
| 이미지 태그 | hardened (고정) | ${{ github.sha }} (동적) | 추적성 및 무결성 강화 |
| 도구 실행 | Actions 라이브러리 위주 | Docker Run 직접 호출 (Gitleaks 등) | 도구 종속성 제거 |
# 파이프라인 명칭: DevSecOps 1~2주차 통합 가드레일
# 주요 목적: 시크릿 탐지, SAST, SCA, 이미지 하드닝, 취약점 리포팅, 이미지 서명 자동화
name: DevSecOps 2-Week Automation Pipeline
env:
IMAGE_TAG: ${{github.sha}}
permissions:
contents: read # 코드 스캔을 위해 리포지토리 콘텐츠 읽기 권한 필요
id-token: write # SonarCloud 인증을 위한 OIDC 토큰 발급 권한
pull-requests: write # PR에 보안 분석 결과 코멘트 작성 권한 (옵션)
security-events: write # checkov나 trivy 결과를 github security tab에 업로드
on:
push:
branches: [ "main" ] # main 브랜치에 코드 푸시 시 자동 실행
pull_request:
branches: [ "main" ] # PR 생성 시 보안 검사 강제화
jobs:
pipeline:
name: Security Build & Supply Chain
runs-on: self-hosted # 자체 호스팅 러너에서 실행되어 더 강력한 보안 검사
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0 # 모든 커밋 히스토리를 가져와 Gitleaks가 전체 이력을 스캔할 수 있도록 설정
# ---------------------------------------------------------
# 1. Vault Secret Injection (환경 변수 주입)
# ---------------------------------------------------------
- name: Import Secrets from Vault
uses: hashicorp/vault-action@v3
with:
url: ${{ secrets.VAULT_ADDR }}
tlsSkipVerify: true
method: approle
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
secrets: |
secret/data/infra/sonarqube host_url | SONAR_HOST_URL ;
secret/data/infra/sonarqube token | SONAR_TOKEN ;
secret/data/infra/sonarqube org_key | SONAR_ORG_KEY ;
secret/data/infra/sonarqube project_key | SONAR_PROJECT_KEY ;
secret/data/infra/common snyk_token | SNYK_TOKEN ;
secret/data/infra/common snyk_org_id | SNYK_ORG_ID ;
secret/data/infra/common dockerhub_username | DOCKERHUB_USERNAME ;
secret/data/infra/common dockerhub_token | DOCKERHUB_TOKEN ;
secret/data/infra/common gh_token | GH_TOKEN ;
secret/data/security/cosign key | COSIGN_KEY ;
secret/data/security/cosign password | COSIGN_PASSWORD
# ---------------------------------------------------------
# 2. Static Analysis (Gitleaks, SonarQube, Snyk, Checkov)
# ---------------------------------------------------------
# 2-1. Gitleaks: 커밋 히스토리 내 평문 시크릿(API Key, Password 등) 탐지
- name: Gitleaks Scan
run: |
# ${PWD} 대신 ${{ github.workspace }}를 사용하여 경로 무결성 확보
/usr/bin/docker run --rm \
-v ${{ github.workspace }}:/path \
zricethezav/gitleaks:latest detect --source="/path" -v --exit-code=2
# 2-2. SonarQube: 정적 코드 분석을 통해 취약한 코드 패턴(XSS, SQLi 등) 탐지
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ env.GH_TOKEN }}
SONAR_TOKEN: ${{ env.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }}
-Dsonar.organization=${{ env.SONAR_ORG_KEY }}
-Dsonar.host.url=${{ env.SONAR_HOST_URL }}
-Dsonar.qualitygate.wait=false # 분석 완료를 기다리지 않고 비동기로 진행
# SonarQube 분석 결과를 바탕으로 품질 게이트 통과 여부 확인 (사용자 정의 스크립트)
- name: Custom Security Gate Check
run: python check_gate.py "${{ env.SONAR_PROJECT_KEY }}" "${{ env.SONAR_TOKEN }}"
# 2-3. Snyk: 오픈소스 라이브러리(SCA) 내 알려진 취약점(CVE) 스캔
- name: Run Snyk Test
continue-on-error: true # 학습용 PyGoat은 취약점이 많으므로 중단 없이 결과만 확인
env:
SNYK_TOKEN: ${{ env.SNYK_TOKEN }}
run: |
# requirements.txt 기반으로 라이브러리 보안성 검사
snyk test --file=requirements.txt --org=${{ env.SNYK_ORG_ID }} --skip-unresolved=true
# 2-4. Checkov: Infra(Terraform) 및 Container(Dockerfile) 보안 스캔
- name: Run Checkov Scan (Full IaC Scrutiny)
id: checkov
uses: bridgecrewio/checkov-action@v12.2983.0 # v3 계열의 안정된 버전 사용 (공급망 보안 버전 고정 원칙)
with:
directory: . # 프로젝트 루트(.) 내의 모든 테라폼 파일과 Dockerfile을 스캔 대상으로 지정
skip_path: dockerized_labs # 실습 예제 제외
soft_fail: false # false: 보안 결함(Failed) 발견 시 Exit Code 1을 발생시켜 파이프라인 즉시 중단
framework: all # 테라폼과 Docker를 포함한 모든 지원 프레임워크 자동 탐지
output_format: cli # 보안 분석 결과를 CLI 형태로 상세 출력
# ---------------------------------------------------------
# 3. Build & Hardening & Supply Chain (Trivy, Syft, Cosign)
# ---------------------------------------------------------
#3-1 Docker 이미지 빌드 (Hardened Tag 적용) Dockerfile의 멀티 스테이지 빌드와 비특권 사용자 설정이 반영된 이미지 생성
- name: Build Docker Image
run: |
docker build -t ${{ env.DOCKERHUB_USERNAME }}/pygoat:${{ env.IMAGE_TAG }} .
#3-2 Trivy: 빌드된 이미지 내 OS 패키지 및 라이브러리 취약점 진단
- name: Run Trivy Image Scan (Report Only)
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.DOCKERHUB_USERNAME }}/pygoat:${{ env.IMAGE_TAG }}'
format: 'table'
exit-code: '0' # 취약점이 발견되어도 리포트 생성 후 빌드 지속 (사용자 요청 반영)
severity: 'CRITICAL,HIGH' # 고위험군 취약점 집중 출력
# 리포트 저장을 위한 JSON 포맷 스캔 별도 실행
- name: Generate JSON Report for Artifact
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.DOCKERHUB_USERNAME }}/pygoat:${{ env.IMAGE_TAG }}'
format: 'json'
output: 'trivy-report.json'
exit-code: '0'
# Artifact 업로드: 파이프라인 종료 후 GitHub Actions 결과 탭에서 취약점 리포트 다운로드 가능
- name: Upload Trivy Scan Report
uses: actions/upload-artifact@v4
with:
name: trivy-vulnerability-report
path: trivy-report.json
#3-3 Docker Hub 로그인 및 푸시
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Push Docker Image
run: docker push ${{ env.DOCKERHUB_USERNAME }}/pygoat:${{ env.IMAGE_TAG }}
#3-4 Syft: 소프트웨어 자재 명세서(SBOM) 생성 (공급망 보안 가시성 확보)
- name: Generate SBOM (Syft)
run: |
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
anchore/syft ${{ env.DOCKERHUB_USERNAME }}/pygoat:${{ env.IMAGE_TAG }} -o spdx-json > sbom.spdx.json
#3-5. Cosign: 이미지 무결성 증명을 위한 전자 서명 및 SBOM 어테스테이션
- name: Sign & Attest Image
run: |
printf "%s" "${{ env.COSIGN_KEY }}" > cosign.key # 환경 변수의 개인키를 파일로 생성
# 이미지 서명: 배포된 이미지가 변조되지 않았음을 증명
cosign sign --key cosign.key -y ${{ env.DOCKERHUB_USERNAME }}/pygoat:${{ env.IMAGE_TAG }}
# SBOM 어테스테이션: 빌드 당시의 의존성 정보를 이미지에 영구 결합 (VEX 등에 활용)
cosign attest --key cosign.key --type spdxjson --predicate sbom.spdx.json -y ${{ env.DOCKERHUB_USERNAME }}/pygoat:${{ env.IMAGE_TAG }}
env:
COSIGN_KEY: ${{ env.COSIGN_KEY }}
COSIGN_PASSWORD: ${{ env.COSIGN_PASSWORD }}

