EC2 해킹당하고, DevSecOps 파이프라인을 구축하다
사이드 프로젝트 개발 서버가 털린 경험을 계기로 DevSecOps 파이프라인을 구축한 이야기입니다.
사이드 프로젝트를 하고 있는데, 어느 날 AWS에서 메일이 날아왔습니다.
Subject: Your AWS account may be compromised
Hello,
Please review this important message regarding the security of yourAWS account and take action as requested.
We have received one or more reports that the following AWS resources:
AWS ID: 44051xxxxx19 Region: ap-northeast-2 EC2 Instance Id: i-0244d4851bxxxxxx
have been implicated in activity that indicates that it may beinfected with malware and may be part of a botnet.
Please be aware, operating a host that is a part of a maliciousnetwork, or "botnet", is forbidden per the AWS Acceptable Use Policy.
---Beginning of forwarded report(s)---
Details of the abusive activity: Report begin time: 2025-12-16 17:14:16 UTC Report end time: 2025-12-16 17:16:15 UTC Average Gbits/sec sent: 0.509
It appears the instance(s) may be compromised and triggered an attack.
Regards,AWS Trust & Safety처음엔 “또 스팸인가?” 싶었는데, 확인해보니 진짜였습니다. 개발용 EC2가 0.5 Gbps로 공격 트래픽을 뿜어내고 있었고, AWS가 이미 일부 포트를 차단한 상태였습니다.
다행히 개발 환경이라 실제 피해는 없었지만, 이참에 DevSecOps를 제대로 구축해보기로 했습니다.
1부: 무슨 일이 있었나
확인해보니
AWS 메일을 받고 서버를 확인해보니, CPU 사용량이 비정상적으로 높았습니다. 컨테이너를 하나씩 확인해봤는데, PostgreSQL 컨테이너 내부에서 암호화폐 채굴 프로세스가 돌고 있었습니다.
# 확인된 악성 프로세스kdevtmpfsi # Monero 마이너
# 확인된 악성 파일/tmp/mysql # 9.5MB - 위장된 악성코드/tmp/init # 3.7MB - 초기화 스크립트Redis에서도 외부 스크립트를 다운로드하려는 cron 관련 데이터가 발견되었습니다.
원인은
개발 환경이라 편의성을 위해 보안 그룹 설정을 느슨하게 해두었던 게 결국 문제가 되었습니다.
flowchart LR subgraph sg["개발환경 보안 그룹 (문제)"] pg["PostgreSQL<br>:5432"] redis["Redis<br>:6379"] pgadmin["pgAdmin<br>:15432"] end
internet(("인터넷<br>0.0.0.0/0"))
internet -->|"❌ 허용"| pg internet -->|"❌ 허용"| redis internet -->|"❌ 허용"| pgadmin일단 수습하기
바로 다음 조치를 수행했습니다.
1단계: 악성 프로세스 종료 및 파일 제거
# 악성 프로세스 종료pkill -f kdevtmpfsi
# 악성 파일 제거rm -f /tmp/mysql /tmp/init2단계: 보안 그룹 차단
flowchart LR subgraph sg["개발환경 보안 그룹 (수정)"] pg["PostgreSQL<br>:5432"] redis["Redis<br>:6379"] pgadmin["pgAdmin<br>:15432"] end
vpc(("VPC 내부<br>10.0.0.0/16"))
vpc -->|"✅ 내부만 허용"| pg vpc -->|"✅ 내부만 허용"| redis vpc -->|"✅ 내부만 허용"| pgadmin3단계: 컨테이너 재구성
감염된 컨테이너는 신뢰할 수 없으므로 새로운 이미지로 완전히 재구성했습니다.
# 기존 컨테이너 제거docker-compose down -v
# 새로운 이미지로 재구성docker-compose up -d --force-recreate4단계: 호스트 시스템 점검
혹시 호스트까지 뚫렸나 확인해봤습니다.
| 점검 항목 | 결과 |
|---|---|
| Crontab | 이상 없음 |
| SSH authorized_keys | 이상 없음 |
| 사용자 계정 | 이상 없음 |
| systemd 서비스 | 이상 없음 |
| /tmp, /var/tmp | 이상 없음 |
다행히 컨테이너 레벨에서 막혔습니다.
2부: 이참에 DevSecOps 구축하기
코드 레벨은 괜찮을까?
인프라 설정은 수동으로 고쳤습니다. 그런데 이번 일을 계기로 생각해보니, 코드 레벨 보안은 어떤 상태인지 모르겠더라고요.
- 의존성 라이브러리에 알려진 취약점이 있으면?
- 시크릿이 하드코딩되어 있으면?
- 컨테이너 이미지에 취약점이 있으면?
이런 것들은 보안 그룹으로 막을 수 있는 게 아닙니다. 코드를 푸시할 때마다 자동으로 스캔하는 체계가 필요했습니다.
도구 구성
세 가지 도구를 Kubernetes에 Helm으로 배포했습니다.
flowchart TB subgraph pipeline["CI/CD Pipeline"] direction TB subgraph scanners["보안 스캐너"] trivy["Trivy<br>(SCA)"] sonarqube["SonarQube<br>(SAST)"] end defectdojo["DefectDojo<br>(통합 관리)"] discord["Discord 알림"]
trivy --> defectdojo sonarqube --> defectdojo defectdojo --> discord end| 도구 | 역할 |
|---|---|
| Trivy | 의존성/이미지 취약점 스캔 (SCA) |
| SonarQube | 소스코드 정적 분석, 시크릿 탐지 (SAST) |
| DefectDojo | 스캔 결과 통합 관리 |

CI/CD 파이프라인 연동하기
보안 스캔 도구를 GitHub Actions에 통합했습니다. 아래는 전체 파이프라인 중 보안 스캔 부분을 발췌한 것입니다.

전체 파이프라인 구성:
| Job | 역할 | 의존성 |
|---|---|---|
trivy-scan | 의존성 취약점 스캔 → DefectDojo 업로드 | - |
sonarqube-scan | 코드 정적 분석 → Quality Gate → DefectDojo 업로드 | - |
docker-build-api | API 이미지 빌드/푸시 (GHCR) | - |
docker-build-batch | Batch 이미지 빌드/푸시 (GHCR) | - |
deploy | EC2 SSH 배포 + Health 검증 | docker-build-* |
notify | Discord 알림 (성공/실패/취소 분기) | 전체 |
보안 스캔 Job 발췌:
name: CI/CD Pipeline
on: pull_request: branches: [main, develop] push: branches: [main] tags: ["v*.*.*"]
env: TRIVY_CRITICAL_THRESHOLD: 2 TRIVY_HIGH_THRESHOLD: 25 BLOCK_ON_FAILURE: false
jobs: trivy-scan: name: Trivy Scan runs-on: ubuntu-latest outputs: critical_count: ${{ steps.parse.outputs.critical_count }} high_count: ${{ steps.parse.outputs.high_count }} summary: ${{ steps.parse.outputs.summary }}
steps: - uses: actions/checkout@v4
- name: Run Trivy uses: aquasecurity/trivy-action@master with: scan-type: "fs" scan-ref: "." trivy-config: .github/config/trivy.yaml format: "json" output: "trivy-results.json" env: TRIVY_SERVER: ${{ secrets.TRIVY_SERVER_URL }} TRIVY_TOKEN: ${{ secrets.TRIVY_TOKEN }} continue-on-error: true
- name: Parse Results id: parse run: | CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy-results.json) HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' trivy-results.json) echo "critical_count=$CRITICAL" >> $GITHUB_OUTPUT echo "high_count=$HIGH" >> $GITHUB_OUTPUT
- name: Check Thresholds run: | if [ "$BLOCK_ON_FAILURE" = "true" ] && [ "$CRITICAL_COUNT" -gt "$TRIVY_CRITICAL_THRESHOLD" ]; then echo "CRITICAL threshold exceeded" exit 1 fi
- name: Upload to DefectDojo if: always() run: | curl -X POST "${DEFECTDOJO_URL}/api/v2/import-scan/" \ -H "Authorization: Token ${DEFECTDOJO_TOKEN}" \ -F "scan_type=Trivy Scan" \ -F "file=@trivy-results.json" \ -F "product_name=SYC Backend" \ -F "engagement_name=CI Security Scan"
sonarqube-scan: name: SonarQube runs-on: ubuntu-latest outputs: quality_gate: ${{ steps.qg.outputs.quality-gate-status }}
steps: - uses: actions/checkout@v4 with: fetch-depth: 0
- name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: "21" distribution: "temurin" cache: gradle
- name: Build with Gradle run: ./gradlew build -x test
- name: SonarQube Scan uses: sonarsource/sonarqube-scan-action@master env: SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Quality Gate id: qg uses: sonarsource/sonarqube-quality-gate-action@master continue-on-error: true
- name: Upload to DefectDojo if: always() run: | curl -s -u "${SONAR_TOKEN}:" \ "${SONAR_HOST_URL}/api/issues/search?componentKeys=syc-be" \ -o sonar-issues.json
curl -X POST "${DEFECTDOJO_URL}/api/v2/import-scan/" \ -H "Authorization: Token ${DEFECTDOJO_TOKEN}" \ -F "scan_type=SonarQube Scan" \ -F "file=@sonar-issues.json" \ -F "product_name=SYC Backend" \ -F "engagement_name=CI Security Scan"
# docker-build-api, docker-build-batch, deploy job은 생략
notify: name: Notify needs: [trivy-scan, sonarqube-scan, docker-build-api, docker-build-batch, deploy] if: always()
steps: - name: Discord env: CRIT: ${{ needs.trivy-scan.outputs.critical_count }} HIGH: ${{ needs.trivy-scan.outputs.high_count }} QG: ${{ needs.sonarqube-scan.outputs.quality_gate }} run: | # 성공/실패/취소 분기 처리 후 Discord Embed 전송 curl -H "Content-Type: application/json" -d "{ \"embeds\": [{ \"title\": \"✅ 배포 완료\", \"fields\": [ {\"name\": \"Trivy\", \"value\": \"Critical: $CRIT, High: $HIGH\"}, {\"name\": \"SonarQube\", \"value\": \"Quality Gate: $QG\"} ] }] }" "$WEBHOOK"3부: 실제로 써보니
의존성 취약점 17건 발견
파이프라인을 구축하고 처음 돌려봤는데, Trivy가 HIGH 심각도 취약점 17건을 탐지했습니다.

탐지된 취약점:
| CVE ID | 심각도 | 패키지 | 설명 |
|---|---|---|---|
| CVE-2025-41249 | HIGH | spring-core | Spring Framework 보안 취약점 |
| CVE-2025-48988 | HIGH | tomcat-embed-core | Apache Tomcat 보안 취약점 |
| CVE-2025-48989 | HIGH | tomcat-embed-core | Apache Tomcat 보안 취약점 |
| … | HIGH | (기타 14건) |
총계: Critical 0건, High 17건
모두 Spring Boot와 관련된 의존성 취약점이었습니다.
해결 방법:
plugins { // 취약점이 패치된 버전으로 업그레이드 id 'org.springframework.boot' version '3.4.1'}결과:
| 지표 | Before | After |
|---|---|---|
| Critical | 0건 | 0건 |
| High | 17건 | 0건 |
단순히 Spring Boot 버전을 업그레이드하는 것만으로 17건의 취약점이 해결되었습니다.
하드코딩된 시크릿도 잡아냄

SonarQube가 BLOCKER 심각도의 보안 이슈를 탐지했습니다.

탐지 내용:
심각도: BLOCKER유형: Hardcoded Secret위치: application-local.yml내용: 비밀번호 하드코딩 탐지로컬 설정 파일에 하드코딩해둔 비밀번호가 탐지된 것입니다. 이런 파일이 실수로 커밋되면 문제가 될 수 있습니다.
해결 방법:
spring: datasource: password: mypassword123 # 하드코딩된 비밀번호
# After: application-local.ymlspring: datasource: password: ${DB_PASSWORD} # 환경변수로 분리추가로 .gitignore에 민감한 파일을 추가하고, GitHub Secrets를 활용하도록 변경했습니다.
실시간 알림 체계 구축하기
보안 스캔 결과를 팀 전체가 확인할 수 있도록 Discord 알림을 설정했습니다.
취약점 발견 시:

취약점 해결 후:

4부: 정리
도구별 역할 정리
두 도구는 표준은 비슷하게 지원하지만, 스캔 대상이 다릅니다.
| 도구 | 스캔 대상 | 찾는 것 | 이번에 발견한 것 |
|---|---|---|---|
| Trivy | 의존성, 컨테이너 이미지 | 알려진 취약점 (CVE) | Spring Boot 취약점 17건 |
| SonarQube | 소스코드 | 버그, 시크릿, 코드 품질 | 하드코딩된 비밀번호 |
DefectDojo는 두 도구의 결과를 한 곳에서 관리하고 보고서를 생성합니다.

배운 것
- 의존성 업데이트만 해도 취약점이 사라진다 - Spring Boot 버전 올리는 것만으로 17건이 해결됐습니다.
- 무료로 쓰려면 통합 도구가 필요하다 - SonarQube Developer Edition 등 유료 라이선스를 쓰면 보고서 기능이 있지만, Community Edition만으로는 부족합니다. DefectDojo를 붙이면 무료로 통합 관리와 보고서 생성이 가능합니다.
- 인프라 보안도 자동화해야 한다 - 이번엔 보안 그룹을 수동으로 고쳤는데, IaC로 관리하면 인프라 설정도 코드 리뷰 대상이 됩니다.
다음에 해볼 것:
- IaC 도입 후 인프라 보안 스캔 (Checkov, tfsec)
- DAST 도입
참고 자료

관련 콘텐츠
LinkedIn에서 발견한 Tencent WeKnora, GraphRAG PoC하고 PR까지 Merged
LinkedIn에서 발견한 Tencent WeKnora를 홈 Kubernetes 클러스터에서 PoC하고, Helm Chart PR까지 Merge한 여정
KubernetesGateway API 전환기 (1) - Cilium을 Kubespray에서 Helm으로
Kubespray로 설치한 Cilium을 Helm 관리로 전환하는 과정에서 겪은 트러블슈팅과 교훈을 공유합니다.
ObservabilityHTTP/2와 gRPC 이해하기
HTTP/2 프레임 구조부터 gRPC 동작 방식, Protobuf 인코딩까지 직접 확인하며 이해합니다.