보안 스캐너가 CVE 목록을 쏟아내면 어디서부터 손대야 할지 막막해진다. 중요한 것은 모든 취약점을 즉시 고치는 것이 아니라, 리스크를 기준으로 우선순위를 매기고, 체계적으로 조치하고, 재발을 방지하는 프로세스를 갖추는 것이다.
이 글에서는 OS 패키지 취약점 패치, 컨테이너 이미지 보안, 런타임 하드닝을 아우르는 실전 플레이북을 제시한다.
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 발견 │ → │ 분류 │ → │ 조치 │ → │ 검증 │ → │ 보고 │
│ Scanning │ │ Triage │ │ Remediate│ │ Verify │ │ Report │
└─────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
| 심각도 | CVSS 점수 | 조치 기한 | 예시 |
|---|
| Critical | 9.0~10.0 | 24~72시간 | RCE, 인증 우회 |
| High | 7.0~8.9 | 7일 | 권한 상승, 정보 유출 |
| Medium | 4.0~6.9 | 30일 | XSS, 서비스 거부 |
| Low | 0.1~3.9 | 90일 또는 다음 정기 패치 | 정보 노출 (제한적) |
핵심 원칙: CVSS 점수만으로 판단하지 말고, Exploit 가능 여부(EPSS), 인터넷 노출 여부, 영향받는 자산의 중요도를 종합 판단한다.
dnf updateinfo list security
dnf updateinfo list --sec-severity=Critical
dnf updateinfo info RHSA-2026:1234
apt update
apt list --upgradable 2>/dev/null | grep -i security
trivy rootfs --severity CRITICAL,HIGH /
oscap xccdf eval \
--profile xccdf_org.ssgproject.content_profile_cis \
--results results.xml \
--report report.html \
/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
#!/usr/bin/env bash
set -euo pipefail
LOG_FILE="/var/log/security-patch-$(date +%Y%m%d).log"
SNAPSHOT_NAME="pre-patch-$(date +%Y%m%d-%H%M%S)"
log() { printf '[%s] %s\n' "$(date +%T)" "$1" | tee -a "$LOG_FILE"; }
log "=== 패치 시작 ==="
log "스냅샷 생성: $SNAPSHOT_NAME"
rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' | sort > /tmp/packages-before.txt
log "보안 업데이트 적용 중..."
if command -v dnf &>/dev/null; then
dnf update --security -y 2>&1 | tee -a "$LOG_FILE"
elif command -v apt-get &>/dev/null; then
apt-get update
apt-get upgrade -y -o Dpkg::Options::="--force-confold" 2>&1 | tee -a "$LOG_FILE"
fi
rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' | sort > /tmp/packages-after.txt
diff /tmp/packages-before.txt /tmp/packages-after.txt > /tmp/packages-diff.txt || true
log "변경된 패키지:"
cat /tmp/packages-diff.txt | tee -a "$LOG_FILE"
if command -v needs-restarting &>/dev/null; then
log "재시작 필요 서비스:"
needs-restarting -s 2>&1 | tee -a "$LOG_FILE"
if needs-restarting -r 2>&1 | grep -q "Reboot is required"; then
log "WARNING: 커널 업데이트로 리부팅 필요"
fi
fi
log "=== 패치 완료 ==="
dnf install -y dnf-automatic
systemctl enable --now dnf-automatic-install.timer
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
| 도구 | 라이선스 | 특징 | CI/CD 통합 |
|---|
| Trivy | OSS (Apache-2.0) | OS + 언어 패키지 + IaC + Secret | GitHub Actions, GitLab CI |
| Grype | OSS (Apache-2.0) | SBOM 기반 스캔 | CLI 중심 |
| Snyk Container | 상용 (무료 플랜) | 개발자 친화적 Fix PR | 대부분 CI |
| Prisma Cloud | 상용 | 전체 CNAPP | 엔터프라이즈 CI |
| AWS ECR Scanning | AWS 포함 | Inspector 기반 | AWS 네이티브 |
trivy image --severity CRITICAL,HIGH myapp:latest
trivy image --format spdx-json -o sbom.json myapp:latest
trivy image --exit-code 1 --severity CRITICAL myapp:latest
FROM cgr.dev/chainguard/python:latest-dev AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
COPY . .
FROM cgr.dev/chainguard/python:latest
WORKDIR /app
COPY --from=builder /install /usr/local
COPY --from=builder /app .
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
ENTRYPOINT ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0"]
| 이미지 유형 | 크기 | 취약점 수 | 패키지 매니저 | 쉘 | 추천 용도 |
|---|
ubuntu:24.04 | ~78MB | 중간 | apt | bash | 범용 |
alpine:3.20 | ~7MB | 낮음 | apk | ash | 경량 서비스 |
distroless | ~20MB | 매우 낮음 | 없음 | 없음 | 프로덕션 런타임 |
chainguard | ~15MB | 거의 0 | apk (dev만) | 없음(dev만) | 보안 중시 |
ubi-micro | ~35MB | 낮음 | 없음 | 없음 | RHEL 호환 필요 |
scratch | 0MB | 0 | 없음 | 없음 | Go 바이너리 |
name: Container Security
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: CRITICAL,HIGH
exit-code: 1
format: sarif
output: trivy-results.sarif
- name: Upload scan results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
- name: Generate SBOM
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: spdx-json
output: sbom.spdx.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
{
"userns-remap": "default",
"no-new-privileges": true,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"live-restore": true,
"icc": false,
"default-ulimits": {
"nofile": { "Name": "nofile", "Hard": 65535, "Soft": 65535 }
}
}
docker run -d \
--name myapp \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=100m \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges:true \
--security-opt seccomp=default.json \
--pids-limit 100 \
--memory 512m \
--cpus 1.0 \
--user 1000:1000 \
myapp:latest
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ['ALL']
resources:
limits:
memory: 512Mi
cpu: '1'
requests:
memory: 256Mi
cpu: '0.5'
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
sizeLimit: 100Mi
| 영역 | 항목 | 명령/설정 |
|---|
| 인증 | 익명 접근 차단 | --anonymous-auth=false |
| 인가 | RBAC 활성화 | --authorization-mode=RBAC |
| API | NodeRestriction 어드미션 | --enable-admission-plugins=NodeRestriction |
| Etcd | 암호화 설정 | --encryption-provider-config |
| 네트워크 | NetworkPolicy 적용 | Calico/Cilium 네트워크 정책 |
| Pod | PSA (Pod Security Admission) | 네임스페이스에 enforce: restricted |
| 시크릿 | 외부 시크릿 관리자 | Vault, AWS Secrets Manager |
| 감사 | Audit 로깅 | --audit-log-path, --audit-policy-file |
| 이미지 | 서명 검증 | Cosign + Admission Webhook |
| 런타임 | Seccomp/AppArmor 프로파일 | Pod securityContext |
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-traffic
namespace: production
spec:
podSelector:
matchLabels:
app: myapp
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: nginx-ingress
ports:
- port: 8080
protocol: TCP
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- port: 5432
protocol: TCP
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
1. [즉시] CVE 공지 확인 → 영향받는 패키지·버전 식별
2. [1시간] 자산 인벤토리에서 영향 범위 파악
3. [2시간] 패치 가용 여부 확인
├── 패치 있음 → 스테이징 적용 → 테스트 → 프로덕션 롤링
└── 패치 없음 → Workaround 적용 (WAF 룰, 설정 변경, 서비스 격리)
4. [24시간] 프로덕션 패치 완료
5. [48시간] 스캔으로 조치 검증 → 보고서 작성
1. 이미지 스캔 결과 확인 (Trivy/Snyk)
2. 취약점 분류:
├── OS 패키지 → 베이스 이미지 업데이트
├── 언어 라이브러리 → dependency 업데이트
└── 설정 문제 → Dockerfile 수정
3. 이미지 재빌드 + 스캔 재실행
4. 스테이징 배포 → E2E 테스트
5. 프로덕션 롤링 업데이트
6. 이전 이미지 태그 비활성화
1. [즉시] 알림 확인 (Falco, 감사 로그)
2. [즉시] 해당 Pod/컨테이너 격리 (NetworkPolicy deny-all)
3. [1시간] 포렌식 데이터 수집:
- 컨테이너 프로세스 목록, 네트워크 연결
- 감사 로그, 런타임 이벤트 로그
- 이미지 레이어 분석
4. [4시간] 침입 경로 파악 → 취약점 조치
5. [24시간] 클린 이미지로 재배포
6. [1주] 사후 분석 보고서 + 재발 방지 대책
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 코드 푸시 │ → │ CI 파이프라인 │ → │ 이미지 빌드 │
└─────────────┘ └──────┬──────┘ └──────┬──────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ SAST 스캔 │ │ 이미지 스캔 │
│ (Semgrep) │ │ (Trivy) │
└──────┬──────┘ └──────┬──────┘
│ │
┌──────▼───────────────────▼──────┐
│ 결과 집계 + 판정 │
│ (Critical → 빌드 실패) │
│ (High → 경고 + Jira 생성) │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ 레지스트리 푸시 (통과 시에만) │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ 정기 스캔 (주간, 레지스트리 전체) │
└─────────────────────────────────┘
| 프레임워크 | 관련 요구사항 | 이 플레이북의 대응 |
|---|
| CIS Benchmark | OS·Docker·K8s 벤치마크 | 섹션 2, 4 |
| NIST 800-53 | SI-2 (Flaw Remediation) | 섹션 2 (패치 관리) |
| PCI DSS 4.0 | 6.3 (취약점 식별·조치) | 전체 프로세스 |
| SOC 2 | CC7.1 (모니터링), CC8.1 (변경관리) | 섹션 5, 6 |
| ISMS-P | 2.10 (시스템·서비스 보안관리) | 전체 |
| 용도 | OSS | 상용 |
|---|
| 이미지 스캔 | Trivy, Grype | Snyk, Prisma Cloud |
| OS 컴플라이언스 | OpenSCAP, Lynis | Qualys, Tenable |
| 런타임 보안 | Falco | Sysdig Secure |
| SBOM 생성 | Syft, Trivy | Anchore |
| 시크릿 스캔 | Gitleaks, TruffleHog | GitGuardian |
| SAST | Semgrep, CodeQL | SonarQube, Checkmarx |
| 이미지 서명 | Cosign (Sigstore) | Docker Content Trust |
| 정책 엔진 | OPA/Gatekeeper, Kyverno | Styra DAS |
코드 단계: Gitleaks (시크릿) + Semgrep (SAST)
빌드 단계: Trivy (이미지 스캔) + Syft (SBOM)
배포 단계: Cosign (이미지 서명) + Kyverno (정책)
런타임: Falco (이상 탐지) + 감사 로그
# 보안 취약점 조치 보고서
## 개요
- 보고일: YYYY-MM-DD
- 작성자: 보안 운영팀
- 대상 기간: YYYY-MM-DD ~ YYYY-MM-DD
## 요약
| -------- | ---- | --------- | --------- | ------ |
| Critical | 3 | 3 | 0 | 0 |
| High | 12 | 10 | 2 | 0 |
| Medium | 45 | 30 | 5 | 10 |
## Critical 취약점 상세
### CVE-YYYY-XXXXX
- 영향: [설명]
- 영향 자산: [서버/이미지 목록]
- 조치: [패치 버전/설정 변경]
- 조치일: YYYY-MM-DD
- 검증: [스캔 결과 첨부]
## 예외 처리 목록
| -------------- | --------------------- | ----------- | ---------- |
| CVE-YYYY-XXXXX | 해당 코드 경로 미사용 | WAF 룰 추가 | YYYY-MM-DD |
## 개선 권고사항
1. [구체적 개선 사항]
2. [구체적 개선 사항]
보안은 한 번의 조치가 아니라 지속적인 프로세스다. 이 플레이북의 핵심을 세 줄로 요약한다.
- 자동화: 스캔·패치·검증을 CI/CD에 내장하여 수동 누락을 방지한다.
- 계층 방어: OS·이미지·런타임·네트워크를 모두 다루는 심층 방어 전략을 수립한다.
- 대응 체계: 발견→분류→조치→검증→보고의 사이클을 반복하며, SLA를 지킨다.
완벽한 보안은 불가능하지만, 체계적인 운영으로 리스크를 관리 가능한 수준으로 낮출 수 있다.