클라우드 보안 점검 도구 Prowler는 AWS, Azure, GCP, Kubernetes 환경에서 취약점 및 보안 설정을 손쉽게 점검할 수 있는 강력한 오픈소스 툴입니다. 이번 글에서는 prowlercloud/prowler-api와 prowlercloud/prowler-ui 이미지를 활용해, 도커 컴포즈 기반으로 Prowler 보안 점검 웹 UI 환경을 구축하는 과정을 공유합니다. 실제로 설정하며 겪었던 시행착오와 해결 방법도 정리해두었으니 참고하시기 바랍니다.
1. 왜 Prowler인가?
기업이 클라우드 환경을 운영하다 보면 보안 규정 준수와 취약점 관리가 필수적입니다. 그러나 이를 수동으로 점검하기는 매우 어렵습니다. Prowler는 다음과 같은 장점을 제공합니다:
- 멀티 클라우드 지원: AWS, GCP, Azure, Kubernetes 점검 가능
- 규정 준수: CIS, HIPAA, ISO27001, GDPR 등 주요 규정에 맞춘 체크리스트 제공
- 확장성: CI/CD 파이프라인 및 보안 대시보드 연동 가능
- 오픈소스: 무료로 사용 가능하며, 도커 기반 자체 구축이 용이
2. 기본 구조
Prowler API와 UI는 별도의 컨테이너로 구동됩니다. 추가로 Postgres 데이터베이스, Valkey(Redis 포크), 그리고 Worker/Beat(비동기 작업 실행 및 스케줄링)를 함께 구성하여 완전한 환경을 만듭니다.
구성 요소는 다음과 같습니다:
- api: Django 기반 백엔드 API
- ui: Next.js 기반 웹 프론트엔드
- postgres: 결과 저장용 DB
- valkey: Celery 작업 큐
- worker / worker-beat: 비동기 작업 처리 및 스케줄러
3. Docker Compose 설정
docker-compose.yml
아래는 최종적으로 정상 실행된 docker-compose.yml 예시입니다.
version: "3.9"
services:
api:
hostname: "prowler-api"
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
env_file: .env
ports:
- "${DJANGO_PORT:-8080}:${DJANGO_PORT:-8080}"
volumes:
- "output:/tmp/prowler_api_output"
depends_on:
postgres:
condition: service_healthy
valkey:
condition: service_healthy
entrypoint:
- "/home/prowler/docker-entrypoint.sh"
- "prod"
ui:
image: prowlercloud/prowler-ui:${PROWLER_UI_VERSION:-stable}
env_file: .env
ports:
- "${UI_PORT:-3000}:${UI_PORT:-3000}"
postgres:
image: postgres:16.3-alpine3.20
hostname: "postgres-db"
volumes:
- ./_data/postgres:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${POSTGRES_ADMIN_USER}
- POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
env_file: .env
ports:
- "${POSTGRES_PORT:-5432}:${POSTGRES_PORT:-5432}"
healthcheck:
test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_ADMIN_USER} -d ${POSTGRES_DB}'"]
interval: 5s
timeout: 5s
retries: 5
valkey:
image: valkey/valkey:7-alpine3.19
hostname: "valkey"
volumes:
- ./_data/valkey:/data
env_file: .env
ports:
- "${VALKEY_PORT:-6379}:6379"
healthcheck:
test: ["CMD-SHELL", "sh -c 'valkey-cli ping'"]
interval: 10s
timeout: 5s
retries: 3
worker:
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
env_file: .env
volumes:
- "output:/tmp/prowler_api_output"
depends_on:
valkey:
condition: service_healthy
postgres:
condition: service_healthy
entrypoint:
- "/home/prowler/docker-entrypoint.sh"
- "worker"
worker-beat:
image: prowlercloud/prowler-api:${PROWLER_API_VERSION:-stable}
env_file: .env
depends_on:
valkey:
condition: service_healthy
postgres:
condition: service_healthy
entrypoint:
- "/home/prowler/docker-entrypoint.sh"
- "beat"
volumes:
output:
driver: local
.env 파일
Compose에서 사용할 환경 변수는 .env 파일에 정의합니다. 이 파일은 UI와 API에서 공통으로 활용됩니다. 주요 항목은 아래와 같습니다.
#### Important Note ####
# This file is used to store environment variables for the Prowler App.
# For production, it is recommended to use a secure method to store these variables and change the default secret keys.
#### Prowler UI Configuration ####
PROWLER_UI_VERSION="stable"
AUTH_URL=http://localhost:3000
API_BASE_URL=http://prowler-api:8080/api/v1
NEXT_PUBLIC_API_BASE_URL=${API_BASE_URL}
NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
AUTH_TRUST_HOST=true
UI_PORT=3000
# Temp URL for feeds need to use actual
RSS_FEED_URL=https://prowler.com/blog/rss
# openssl rand -base64 32
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
# Google Tag Manager ID
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
#### Prowler API Configuration ####
PROWLER_API_VERSION="stable"
# PostgreSQL settings
# If running Django and celery on host, use 'localhost', else use 'postgres-db'
POSTGRES_HOST=postgres-db
POSTGRES_PORT=5432
POSTGRES_ADMIN_USER=prowler_admin
POSTGRES_ADMIN_PASSWORD=postgres
POSTGRES_USER=prowler
POSTGRES_PASSWORD=postgres
POSTGRES_DB=prowler_db
# Celery-Prowler task settings
TASK_RETRY_DELAY_SECONDS=0.1
TASK_RETRY_ATTEMPTS=5
# Valkey settings
# If running Valkey and celery on host, use localhost, else use 'valkey'
VALKEY_HOST=valkey
VALKEY_PORT=6379
VALKEY_DB=0
# API scan settings
# The path to the directory where scan output should be stored
DJANGO_TMP_OUTPUT_DIRECTORY="/tmp/prowler_api_output"
# The maximum number of findings to process in a single batch
DJANGO_FINDINGS_BATCH_SIZE=1000
# The AWS access key to be used when uploading scan output to an S3 bucket
# If left empty, default AWS credentials resolution behavior will be used
DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID=""
# The AWS secret key to be used when uploading scan output to an S3 bucket
DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY=""
# An optional AWS session token
DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN=""
# The AWS region where your S3 bucket is located (e.g., "us-east-1")
DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION=""
# The name of the S3 bucket where scan output should be stored
DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET=""
# Django settings
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api
DJANGO_BIND_ADDRESS=0.0.0.0
DJANGO_PORT=8080
DJANGO_DEBUG=False
DJANGO_SETTINGS_MODULE=config.django.production
# Select one of [ndjson|human_readable]
DJANGO_LOGGING_FORMATTER=human_readable
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
# Applies to both Django and Celery Workers
DJANGO_LOGGING_LEVEL=INFO
# Defaults to the maximum available based on CPU cores if not set.
DJANGO_WORKERS=4
# Token lifetime is in minutes
DJANGO_ACCESS_TOKEN_LIFETIME=30
# Token lifetime is in minutes
DJANGO_REFRESH_TOKEN_LIFETIME=1440
DJANGO_CACHE_MAX_AGE=3600
DJANGO_STALE_WHILE_REVALIDATE=60
DJANGO_MANAGE_DB_PARTITIONS=True
# openssl genrsa -out private.pem 2048
DJANGO_TOKEN_SIGNING_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDs4e+kt7SnUJek
6V5r9zMGzXCoU5qnChfPiqu+BgANyawz+MyVZPs6RCRfeo6tlCknPQtOziyXYM2I
7X+qckmuzsjqp8+u+o1mw3VvUuJew5k2SQLPYwsiTzuFNVJEOgRo3hywGiGwS2iv
/5nh2QAl7fq2qLqZEXQa5+/xJlQggS1CYxOJgggvLyra50QZlBvPve/AxKJ/EV/Q
irWTZU5lLNI8sH2iZR05vQeBsxZ0dCnGMT+vGl+cGkqrvzQzKsYbDmabMcfTYhYi
78fpv6A4uharJFHayypYBjE39PwhMyyeycrNXlpm1jpq+03HgmDuDMHydk1tNwuT
nEC7m7iNAgMBAAECggEAA2m48nJcJbn9SVi8bclMwKkWmbJErOnyEGEy2sTK3Of+
NWx9BB0FmqAPNxn0ss8K7cANKOhDD7ZLF9E2MO4/HgfoMKtUzHRbM7MWvtEepldi
nnvcUMEgULD8Dk4HnqiIVjt3BdmGiTv46OpBnRWrkSBV56pUL+7msZmMZTjUZvh2
ZWv0+I3gtDIjo2Zo/FiwDV7CfwRjJarRpYUj/0YyuSA4FuOUYl41WAX1I301FKMH
xo3jiAYi1s7IneJ16OtPpOA34Wg5F6ebm/UO0uNe+iD4kCXKaZmxYQPh5tfB0Qa3
qj1T7GNpFNyvtG7VVdauhkb8iu8X/wl6PCwbg0RCKQKBgQD9HfpnpH0lDlHMRw9K
X7Vby/1fSYy1BQtlXFEIPTN/btJ/asGxLmAVwJ2HAPXWlrfSjVAH7CtVmzN7v8oj
HeIHfeSgoWEu1syvnv2AMaYSo03UjFFlfc/GUxF7DUScRIhcJUPCP8jkAROz9nFv
DByNjUL17Q9r43DmDiRsy0IFqQKBgQDvlJ9Uhl+Sp7gRgKYwa/IG0+I4AduAM+Gz
Dxbm52QrMGMTjaJFLmLHBUZ/ot+pge7tZZGws8YR8ufpyMJbMqPjxhIvRRa/p1Tf
E3TQPW93FMsHUvxAgY3MV5MzXFPhlNAKb+akP/RcXUhetGAuZKLubtDCWa55ZQuL
wj2OS+niRQKBgE7K8zUqNi6/22S8xhy/2GPgB1qPObbsABUofK0U6CAGLo6te+gc
6Jo84IyzFtQbDNQFW2Fr+j1m18rw9AqkdcUhQndiZS9AfG07D+zFB86LeWHt4DS4
ymIRX8Kvaak/iDcu/n3Mf0vCrhB6aetImObTj4GgrwlFvtJOmrYnO8EpAoGAIXXP
Xt25gWD9OyyNiVu6HKwA/zN7NYeJcRmdaDhO7B1A6R0x2Zml4AfjlbXoqOLlvLAf
zd79vcoAC82nH1eOPiSOq51plPDI0LMF8IN0CtyTkn1Lj7LIXA6rF1RAvtOqzppc
SvpHpZK9pcRpXnFdtBE0BMDDtl6fYzCIqlP94UUCgYEAnhXbAQMF7LQifEm34Dx8
BizRMOKcqJGPvbO2+Iyt50O5X6onU2ITzSV1QHtOvAazu+B1aG9pEuBFDQ+ASxEu
L9ruJElkOkb/o45TSF6KCsHd55ReTZ8AqnRjf5R+lyzPqTZCXXb8KTcRvWT4zQa3
VxyT2PnaSqEcexWUy4+UXoQ=
-----END PRIVATE KEY-----"
# openssl rsa -in private.pem -pubout -out public.pem
DJANGO_TOKEN_VERIFYING_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7OHvpLe0p1CXpOlea/cz
Bs1wqFOapwoXz4qrvgYADcmsM/jMlWT7OkQkX3qOrZQpJz0LTs4sl2DNiO1/qnJJ
rs7I6qfPrvqNZsN1b1LiXsOZNkkCz2MLIk87hTVSRDoEaN4csBohsEtor/+Z4dkA
Je36tqi6mRF0Gufv8SZUIIEtQmMTiYIILy8q2udEGZQbz73vwMSifxFf0Iq1k2VO
ZSzSPLB9omUdOb0HgbMWdHQpxjE/rxpfnBpKq780MyrGGw5mmzHH02IWIu/H6b+g
OLoWqyRR2ssqWAYxN/T8ITMsnsnKzV5aZtY6avtNx4Jg7gzB8nZNbTcLk5xAu5u4
jQIDAQAB
-----END PUBLIC KEY-----"
# openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
DJANGO_SENTRY_DSN=
# Sentry settings
SENTRY_ENVIRONMENT=local
SENTRY_RELEASE=local
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.10.0
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
SOCIAL_GOOGLE_OAUTH_CLIENT_ID=""
SOCIAL_GOOGLE_OAUTH_CLIENT_SECRET=""
SOCIAL_GITHUB_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/github"
SOCIAL_GITHUB_OAUTH_CLIENT_ID=""
SOCIAL_GITHUB_OAUTH_CLIENT_SECRET=""
# Single Sign-On (SSO)
SAML_SSO_CALLBACK_URL="${AUTH_URL}/api/auth/callback/saml"
# Lighthouse tracing
LANGSMITH_TRACING=false
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
LANGSMITH_API_KEY=""
LANGCHAIN_PROJECT=""
실제 키 값(DJANGO_TOKEN_SIGNING_KEY, DJANGO_TOKEN_VERIFYING_KEY)은 OpenSSL을 사용해 안전하게 생성하는 것이 권장됩니다.
4. 자주 겪는 오류와 해결법
- env_file 포맷 오류
처음에는env_file항목을path: .env형식으로 넣어 오류가 발생했습니다. Compose는 단순 문자열만 허용하므로 반드시 아래처럼 작성해야 합니다:env_file: .env - entrypoint 경로 문제
일부 서비스에서../docker-entrypoint.sh로 잘못 지정된 경우 실행이 되지 않았습니다. 컨테이너 내부 경로에 맞게/home/prowler/docker-entrypoint.sh로 수정해야 정상 동작합니다. - Windows 경로 이슈
바인드 마운트 경로를 사용할 때 Windows 경로 문제로 실패할 수 있습니다. 가급적 상대경로나 네임드 볼륨을 사용하는 것이 안정적입니다.
5. 실행 및 확인
# 볼륨용 디렉터리 준비
mkdir -p _data/postgres _data/valkey
# 컨테이너 실행
docker compose up -d
# 상태 확인
docker compose ps
docker compose logs -f api
브라우저에서 http://localhost:3000 에 접속하면 Prowler UI를 통해 보안 점검 작업을 수행할 수 있습니다. API 엔드포인트는 http://localhost:8080/api/v1에서 확인 가능합니다.
6. 활용 방안
- 정기 스케줄링: worker-beat를 이용해 자동 점검
- 리포트 관리: HTML/JSON/CSV 리포트를 API 출력 디렉토리에 저장 후 분석
- 대시보드 연동: Security Hub, Splunk, OpenSearch, QuickSight와 연계해 시각화
- CI/CD 통합: 배포 파이프라인에 포함시켜 보안 게이트 역할 수행
참고 자료
- https://prowler.com/
- https://docs.prowler.com/
- https://github.com/prowler-cloud/prowler
마치며
이번 포스팅에서는 docker-compose.yml과 .env 설정을 기반으로 Prowler API/UI 환경을 구축하는 방법을 다뤘습니다. 단순 CLI 기반 점검에서 한 단계 나아가, 웹 UI와 백엔드 API를 통해 더욱 직관적이고 효율적인 보안 점검을 수행할 수 있습니다.
실제 운영 환경에서는 반드시 보안 키 관리, IAM 최소 권한 원칙, 데이터 백업을 고려해야 하며, 필요 시 SaaS 형태의 ProwlerPro도 검토해 볼 만합니다.
클라우드 보안 점검 자동화에 관심 있다면 직접 따라 해 보시길 추천드립니다!