티스토리 뷰
Kubernetes Pod Security Standards: nginx-unprivileged로 보안 강화하기
pak2251 2025. 10. 26. 19:46Kubernetes Pod Security Standards: nginx-unprivileged로 보안 강화하기
작성일: 2025-10-26
태그: Kubernetes, Security, nginx, Pod Security Standards, Best Practices
난이도: 중급
들어가며
imprun.dev는 Kubernetes 기반 서버리스 Cloud Function 플랫폼입니다. Web Console을 React 19 SPA로 마이그레이션하면서, Kubernetes Pod Security Standards: Restricted 수준을 달성하기 위해 nginx 컨테이너를 완전한 non-root로 전환했습니다.
우리가 마주한 질문:
- ❓ 일반 nginx 이미지: Worker는 non-root인데, Master가 root면 진짜 문제가 될까?
- ❓ nginx-unprivileged 이미지: 정말 완전한 non-root일까?
- ❓ 보안 강화의 가치: 성능 오버헤드는 없을까?
검증 과정:
일반
nginx:1.27-alpine분석- Master process: root (PID 1)
- Worker process: nginx (uid 101)
- 80/443 포트 바인딩 필요 (특권 포트)
nginxinc/nginx-unprivileged:1.27-alpine전환- 모든 프로세스 non-root (nginx 사용자)
- 8080/8443 포트 사용 (비특권 포트)
- Pod Security Standards: Restricted 수준 달성
결론:
- ✅ 보안 강화 (컨테이너 탈출 취약점 대응)
- ✅ 규정 준수 (엔터프라이즈 환경 배포 가능)
- ✅ 성능 오버헤드 없음 (Worker 동작 동일)
이 글은 imprun.dev 플랫폼 구축 경험을 바탕으로, nginx의 실제 동작 방식, nginx-unprivileged의 진실, 그리고 Pod Security Standards: Restricted 수준 달성 과정을 공유합니다.
문제 인식: "non-root"의 함정
일반적인 nginx 이미지의 실제 동작
많은 개발자들이 nginx가 이미 "충분히 안전하다"고 생각합니다. 하지만 실제로는 어떨까요?
# nginx:1.27-alpine 컨테이너 내부
$ ps aux
USER PID COMMAND
root 1 nginx: master process nginx -g daemon off;
nginx 29 nginx: worker process
nginx 30 nginx: worker process
nginx 31 nginx: worker process
놀라운 사실:
- ✅ Worker processes는 이미 non-root (nginx 사용자, uid 101)
- ⚠️ Master process만 root로 실행
- 🎯 실제 HTTP 요청은 worker가 처리
"이미 안전하지 않나요?"
맞습니다. Worker가 non-root이므로 대부분의 공격 벡터는 차단됩니다. 하지만:
Master가 root일 때의 위험:
잠재적 위험 시나리오:
1. nginx 설정 파일 파싱 취약점 발견 시
→ Master process(root) 악용 가능
2. 컨테이너 탈출 취약점과 결합 시
→ root 권한으로 호스트 침투 시도 가능
3. Pod Security Standards 위반
→ 엔터프라이즈/금융 환경에서 배포 불가
현실적 위험도:
- 🟢 낮음: nginx 설정 파싱 취약점은 역사적으로 희귀
- 🟡 중간: Defense in Depth (다층 방어) 관점에서는 개선 필요
- 🔴 높음: 규정 준수(컴플라이언스) 환경에서는 치명적
Part 1: nginx vs nginx-unprivileged - 진짜 차이
nginx-unprivileged는 무엇을 바꾸는가?
# Before: nginx:1.27-alpine
FROM nginx:1.27-alpine
EXPOSE 80
# After: nginxinc/nginx-unprivileged:1.27-alpine
FROM nginxinc/nginx-unprivileged:1.27-alpine
EXPOSE 8080
프로세스 비교:
| nginx | nginx-unprivileged | |
|---|---|---|
| Master Process | root (uid 0) | nginx (uid 101) |
| Worker Processes | nginx (uid 101) | nginx (uid 101) |
| Listen Port | 80 (privileged) | 8080 (non-privileged) |
| Capabilities | 필요 시 CHOWN 등 | 없음 (ALL drop) |
"Master만 바뀌는데 의미가 있나요?"
솔직한 답변: 실질적 보안 개선은 제한적입니다.
이유:
- Worker가 공격 표면의 99%
- Worker는 원래도 non-root
- Master 취약점은 극히 드묾
그럼에도 전환해야 하는 이유:
1. Pod Security Standards 준수
# Kubernetes Pod Security Standards
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted # ← 엄격한 보안
Restricted 수준 요구사항:
securityContext:
runAsNonRoot: true # ✅ 필수
runAsUser: 101 # ✅ root(0) 금지
allowPrivilegeEscalation: false # ✅ 권한 상승 차단
capabilities:
drop: [ALL] # ✅ 모든 capability 제거
nginx 공식 이미지는 이를 만족하지 못합니다.
2. Defense in Depth (다층 방어)
공격 시나리오:
1️⃣ 알려지지 않은 nginx 취약점 발견
2️⃣ Master process 악용
3️⃣ 컨테이너 탈출 시도
nginx (root):
❌ Master가 root → 호스트 침투 가능성
nginx-unprivileged:
✅ Master가 non-root → 영향 제한적3. 컴플라이언스 & 감사
금융/의료/정부 환경:
# 보안 감사 도구
$ kube-bench run
[FAIL] 5.2.6 Ensure that the --runAsNonRoot is set to true
Container: console
Image: nginx:1.27-alpine
runAsNonRoot: false # ← 감사 실패
4. 미래 대비
현재 (2025):
nginx Master 취약점: 거의 없음
미래 (202X):
새로운 취약점 발견 가능
→ non-root면 영향 최소화Part 2: 실전 전환 과정
우리의 초기 상황
imprun.dev Console:
- Next.js 15 → React 19 SPA 마이그레이션 완료
- nginx:1.27-alpine으로 정적 파일 서빙
- SSE (Server-Sent Events)로 실시간 로그 스트리밍
문제 발견:
# Pod 실행 실패
$ kubectl logs imprun-console-xxx
nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)
원인: Helm Chart의 securityContext가 runAsNonRoot: true로 설정되어 있었으나, nginx 공식 이미지는 이를 지원하지 않음.
1단계: Dockerfile 수정
# ============================================
# Runtime Stage: nginx로 정적 파일 서빙
# ============================================
FROM nginxinc/nginx-unprivileged:1.27-alpine AS runtime
# 빌드 결과물 복사
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
# nginx 설정 복사
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
# 포트 노출 (nginx-unprivileged는 8080 사용)
EXPOSE 8080
# Health check (8080 포트)
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
# nginx 시작
CMD ["nginx", "-g", "daemon off;"]
핵심 변경:
- 이미지:
nginx:1.27-alpine→nginxinc/nginx-unprivileged:1.27-alpine - 포트:
80→8080(non-privileged port)
2단계: nginx.conf 수정
# imprun.dev Console - nginx Configuration
# React 19 SPA with SSE (Server-Sent Events) support
# nginx-unprivileged 이미지 사용 (non-root, port 8080)
server {
listen 8080; # ← 80에서 8080으로 변경
server_name _;
# 루트 디렉토리
root /usr/share/nginx/html;
index index.html;
# gzip 압축
gzip on;
gzip_types text/plain text/css application/javascript application/json;
# 정적 파일 캐싱
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 실시간 로그 스트리밍 (SSE) 전용 프록시
location ~ ^/v1/apps/[^/]+/logs/ {
proxy_pass http://imprun-server.imprun-system.svc.cluster.local:80;
# SSE 필수 설정
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
chunked_transfer_encoding on;
proxy_set_header X-Accel-Buffering no;
}
# SPA 라우팅
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
주의사항:
- ⚠️ Port 80은 privileged port (root만 바인딩 가능)
- ✅ Port 8080은 unprivileged port (일반 사용자 가능)
- ✅ SSE 프록시는 정상 작동 (포트와 무관)
3단계: Helm Chart 수정
# k8s/imprun/charts/imprun-console/values.yaml
# 서비스 설정
service:
type: ClusterIP
port: 80 # 외부는 여전히 80
targetPort: 8080 # 컨테이너 포트는 8080
# Security Context (Pod-level)
# nginx-unprivileged 이미지 사용 (완전 non-root, uid 101)
securityContext:
runAsNonRoot: true # ✅ non-root 강제
runAsUser: 101 # ✅ nginx-unprivileged 기본 사용자
fsGroup: 101
# Container Security Context
# nginx-unprivileged는 권한 상승 불필요
containerSecurityContext:
allowPrivilegeEscalation: false # ✅ 권한 상승 차단
capabilities:
drop:
- ALL # ✅ 모든 capability 제거
Before vs After:
# Before: root 실행 + CHOWN capability
securityContext:
runAsNonRoot: false # ❌ root 허용
runAsUser: 1000
containerSecurityContext:
capabilities:
add: [CHOWN] # ❌ 파일 소유권 변경 권한
# After: 완전 non-root + capability 없음
securityContext:
runAsNonRoot: true # ✅ non-root 강제
runAsUser: 101
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL] # ✅ 모든 권한 제거
4단계: 배포 및 검증
# 1. Helm upgrade
$ helm upgrade imprun . -n imprun-system -f values.yaml
Release "imprun" has been upgraded.
REVISION: 18
# 2. Pod 정상 실행 확인
$ kubectl get pods -n imprun-system -l app.kubernetes.io/name=imprun-console
NAME READY STATUS RESTARTS AGE
imprun-console-5fcc8f6686-8cswz 1/1 Running 0 45s
# 3. 프로세스 확인 - 모두 nginx 사용자!
$ kubectl exec -n imprun-system imprun-console-5fcc8f6686-8cswz -- ps aux
PID USER COMMAND
1 nginx nginx: master process nginx -g daemon off;
21 nginx nginx: worker process
22 nginx nginx: worker process
23 nginx nginx: worker process
24 nginx nginx: worker process
# 4. UID 확인
$ kubectl exec -n imprun-system imprun-console-5fcc8f6686-8cswz -- id
uid=101(nginx) gid=101(nginx) groups=101(nginx)
# 5. Security Context 적용 확인
$ kubectl get deployment imprun-console -n imprun-system -o json | jq '.spec.template.spec.securityContext'
{
"fsGroup": 101,
"runAsNonRoot": true,
"runAsUser": 101
}
$ kubectl get deployment imprun-console -n imprun-system -o json | jq '.spec.template.spec.containers[0].securityContext'
{
"allowPrivilegeEscalation": false,
"capabilities": {
"drop": ["ALL"]
}
}
성공 지표:
- ✅ Master process도 nginx 사용자 (uid 101)
- ✅ 모든 capabilities 제거됨
- ✅ Privilege escalation 차단
- ✅ Pod Security Standards: Restricted 통과
Part 3: 보안 개선 효과 정리
달성한 보안 수준
| 보안 항목 | Before | After |
|---|---|---|
| Master Process | root (uid 0) | nginx (uid 101) |
| Worker Processes | nginx (uid 101) | nginx (uid 101) |
| Capabilities | CHOWN | 없음 (ALL drop) |
| Privilege Escalation | 가능 | 차단 |
| Pod Security Standards | Baseline | Restricted ✅ |
Pod Security Standards 준수 체크
# Kubernetes Pod Security Standards: Restricted
✅ Non-root Containers
runAsNonRoot: true
runAsUser: 101 (not 0)
✅ Privilege Escalation
allowPrivilegeEscalation: false
✅ Capabilities
capabilities:
drop: [ALL]
✅ Seccomp Profile
seccompProfile:
type: RuntimeDefault
✅ Volume Types
emptyDir, configMap, secret only
컴플라이언스 체크리스트
# CIS Kubernetes Benchmark
✅ 5.2.5 Minimize the admission of containers with allowPrivilegeEscalation
✅ 5.2.6 Ensure that the --runAsNonRoot is set to true
✅ 5.2.7 Minimize the admission of root containers
✅ 5.2.8 Minimize the admission of containers with the NET_RAW capability
✅ 5.2.9 Minimize the admission of containers with capabilities assigned
# PCI-DSS
✅ 2.2.4 Configure system security parameters to prevent misuse
✅ 6.5.8 Improper access control
# NIST SP 800-190
✅ Container Image Security
✅ Container Runtime Security
✅ Host OS and Multi-tenancy
Part 4: 트레이드오프와 한계
비용 (Trade-offs)
1. 포트 변경 필요
# Service는 여전히 80 포트 노출
service:
port: 80 # 외부 접근
targetPort: 8080 # 컨테이너 내부
# 사용자에게는 투명하게 처리됨
# https://portal.imprun.dev (여전히 443 → 80)
영향: 없음 (Kubernetes Service가 포트 매핑 처리)
2. 약간 복잡한 설정
# nginx 이미지: 그대로 사용 가능
FROM nginx:1.27-alpine
EXPOSE 80
# nginx-unprivileged: 포트 변경 필요
FROM nginxinc/nginx-unprivileged:1.27-alpine
EXPOSE 8080 # ← 변경 필요
영향: 초기 설정 시 한 번만 신경 쓰면 됨
3. 이미지 크기 증가?
# 실제 확인
$ docker images | grep nginx
nginxinc/nginx-unprivileged 1.27-alpine 11.2MB
nginx 1.27-alpine 11.0MB
# 차이: 약 200KB (무시 가능)
영향: 없음
한계 (Limitations)
"완벽한 보안"은 아닙니다
여전히 남아있는 위험:
1. nginx 자체의 취약점 (Worker 공격)
→ Worker는 원래도 non-root였음
2. 애플리케이션 레벨 취약점
→ XSS, CSRF 등은 nginx와 무관
3. 컨테이너 탈출 취약점 (Kernel)
→ Kubernetes/Docker 자체 취약점
Master 취약점은 여전히 희귀
nginx 역사 (2004~2025):
Master process 취약점: 거의 없음
Worker process 취약점: 간헐적 발견
→ Master를 non-root로 바꾸는 것은 "추가 보험"Part 5: 언제 전환해야 하는가?
즉시 전환 권장 (High Priority)
다음 환경은 즉시 전환 필요:
✅ 금융 서비스 (PCI-DSS)
✅ 의료 정보 처리 (HIPAA)
✅ 정부/공공기관
✅ 엔터프라이즈 B2B SaaS
✅ Pod Security Standards: Restricted 적용 클러스터
✅ 보안 감사 대상 서비스
점진적 전환 고려 (Medium Priority)
다음 환경은 여유있을 때 전환:
⚠️ 스타트업 MVP 단계
⚠️ 개발/스테이징 환경
⚠️ 내부 서비스 (외부 비공개)
⚠️ 소규모 B2C 서비스
전환 불필요 (Low Priority)
다음 환경은 현재 상태 유지 가능:
🟢 로컬 개발 환경
🟢 일회성 테스트 환경
🟢 레거시 시스템 (변경 위험 큼)
결론 및 권장사항
핵심 요약
- nginx 공식 이미지도 충분히 안전하다 (Worker가 non-root)
- 하지만 Master가 root면 Pod Security Standards 위반
- nginx-unprivileged는 Master도 non-root로 실행
- 실질적 보안 개선은 제한적이지만, 컴플라이언스 필수
실용적 가이드라인
Phase 1: 현재 상태 평가
# Pod Security 체크
$ kubectl get pods -n your-namespace -o json | \
jq '.items[] | select(.spec.securityContext.runAsNonRoot != true) | .metadata.name'
# 감사 도구 실행
$ kubeaudit all --namespace your-namespace
Phase 2: 단계적 전환
Week 1-2: 계획 수립
- Dockerfile 수정 계획
- nginx.conf 포트 변경
- Helm Chart 수정
Week 3: 개발 환경 적용
- 이미지 빌드 및 테스트
- 동작 검증
Week 4: 스테이징 배포
- Helm upgrade
- 통합 테스트
Week 5: 프로덕션 배포
- Blue-Green 배포
- 롤백 계획 준비
Phase 3: 검증 및 모니터링
# 보안 설정 확인
kubectl get deployment your-app -o yaml | grep -A 10 securityContext
# 프로세스 확인
kubectl exec your-pod -- ps aux
# UID 확인
kubectl exec your-pod -- id
마지막 조언
"완벽한 보안은 없지만, 더 나은 보안은 있습니다."
nginx-unprivileged 전환은:
- 보안 개선의 한 단계
- Pod Security Standards 준수
- Defense in Depth 전략의 일부
비용은 적고, 효과는 분명합니다.
새 프로젝트라면 처음부터 nginx-unprivileged를 사용하세요.참고 자료
공식 문서
관련 블로그
imprun.dev 관련
- Frontend: React 19 SPA (Vite 6)
- Backend: NestJS + MongoDB
- Infrastructure: Kubernetes + Helm
- Security: Pod Security Standards Restricted
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| Serena MCP: AI 코딩 어시스턴트를 위한 시맨틱 코드 분석 도구 (0) | 2025.10.26 |
|---|---|
| Cilium devices 파라미터 완벽 가이드: Tailscale 환경에서의 핵심 (0) | 2025.10.26 |
| Oracle Cloud + Tailscale + Kubernetes 완벽 가이드(6) (0) | 2025.10.26 |
| Oracle Cloud + Tailscale + Kubernetes 완벽 가이드(5) (0) | 2025.10.26 |
| Oracle Cloud + Tailscale + Kubernetes 완벽 가이드(4) (0) | 2025.10.26 |
- Total
- Today
- Yesterday
- api gateway
- Next.js
- architecture
- security
- Tailwind CSS
- Tax Analysis
- troubleshooting
- LLM
- frontend
- react
- SHACL
- Rag
- Developer Tools
- claude code
- AI agent
- Kubernetes
- 개발 도구
- ai 개발 도구
- AI Development
- Ontology
- backend
- authentication
- Claude
- authorization
- knowledge graph
- AI
- PYTHON
- workflow
- Go
- LangChain
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
