Copy & Run

OWASP ZAP baseline 스캔으로 내 앱을 점검하고 보고서 읽기

직접 만든 Flask 앱을 컨테이너로 띄우고 ZAP baseline 스캔을 돌려 결과 요약과 HTML 보고서를 읽습니다. 경고별로 보안 헤더와 쿠키 속성을 고친 뒤 다시 스캔하고, 규칙 설정 파일과 종료 코드로 CI 게이트를 만듭니다.

실습 노트 · 2026-10-16 발행 · 11분 · 2026-10 확인

직접 만든 작은 Flask 앱에 ZAP baseline 스캔을 돌려 어떤 경고가 나오는지 읽고, 경고마다 앱을 고쳐 다시 스캔합니다. 마지막에는 규칙 설정 파일과 종료 코드로 빌드를 멈추는 게이트를 만듭니다. 이 글의 앱은 보안 헤더를 일부러 빼 둔 연습용이므로 실제 서비스에 쓰지 않습니다.

스캔은 허가받은 대상에만 합니다. 이 글은 같은 도커 네트워크 안에 띄운 내 앱 하나만 대상으로 하고, 앱은 호스트의 127.0.0.1 로만 열어 둡니다. 남의 서버나 회사 서비스에 허가 없이 스캐너를 돌리면 법적 문제가 될 수 있으니 -t 에는 직접 띄운 연습용 앱 주소만 넣습니다. 코드를 읽는 정적 검사는 CI에 Semgrep과 Bandit 보안 게이트 붙이기, 이미지 취약점 스캔은 Trivy로 컨테이너 이미지 취약점을 스캔하고 빌드 게이트 만들기에서 다루므로 이 글은 실행 중인 앱의 응답만 봅니다.

준비물

항목 버전 용도
Docker 29.5.3 (Docker Desktop) 앱과 ZAP 실행
ZAP 이미지 ghcr.io/zaproxy/zaproxy:stable (ZAP 2.17.0, 다이제스트 sha256:5ccfff9a...b2b6) baseline 스캔
앱 이미지 python:3.13-slim, Flask 3.1.2, gunicorn 23.0.0 스캔 대상
셸 Windows의 Git Bash 명령 실행

ZAP 이미지는 docker images 기준 약 3.8GB 입니다. 디스크와 내려받을 시간을 먼저 확인합니다. GHCR(ghcr.io)에 접속할 수 없는 환경에서는 공식 문서가 같은 이미지로 안내하는 Docker Hub 의 zaproxy/zap-stable 로 바꿔 쓸 수 있습니다(이 글에서는 GHCR 이미지만 실행했습니다). stable 태그는 새 릴리스와 월간 재빌드 때 바뀌므로 재현하려면 위 다이제스트로 고정합니다(ghcr.io/zaproxy/zaproxy@sha256:5ccfff9a411743ab2615136a2edd7416bec9ce41572fa94e935df38c14adb2b6). 이 글의 명령은 읽기 쉽게 stable 태그로 적습니다. 예상 소요 시간은 30분 안팎입니다. 스캔 한 번은 40초쯤 걸렸는데, 이 앱은 페이지가 적어 spider 가 기본 제한 시간인 1분이 되기 전에 끝났기 때문입니다.

1. 보안 설정이 약한 앱 띄우기

작업 폴더와 앱을 만듭니다. 쿠키에는 HttpOnly, SameSite 가 없고 응답 헤더에는 보안 헤더가 하나도 없습니다.

mkdir -p zap-lab/app zap-lab/wrk && cd zap-lab

cat > app/app.py <<'PY'
from flask import Flask, make_response, request

app = Flask(__name__)


@app.get("/")
def index():
    resp = make_response(
        "<html><head><title>메모 앱</title></head><body>"
        "<h1>메모 앱</h1><p><a href='/login'>로그인</a> | <a href='/memo?q=hello'>메모 검색</a></p>"
        "</body></html>"
    )
    resp.set_cookie("session", "demo-session-value")
    return resp


@app.get("/login")
def login():
    return "<html><head><title>로그인</title></head><body><h1>로그인</h1><a href='/'>처음으로</a></body></html>"


@app.get("/memo")
def memo():
    q = request.args.get("q", "")
    return f"<html><head><title>메모</title></head><body><h1>검색어</h1><p>{q}</p><a href='/'>처음으로</a></body></html>"
PY

cat > app/requirements.txt <<'T'
flask==3.1.2
gunicorn==23.0.0
T

cat > app/Dockerfile <<'T'
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]
T

docker network create cr-zap-net
docker build -t cr-zap-app:v1 app
docker run -d --name cr-zap-app --network cr-zap-net -p 127.0.0.1:20200:5000 cr-zap-app:v1
curl -si http://127.0.0.1:20200/ | head -8

-p 127.0.0.1:20200:5000 은 내 PC 에서만 접속되게 합니다. 응답은 다음과 같습니다.

응답에 X-Content-Type-Options, Content-Security-Policy 같은 보안 헤더가 없고 Set-Cookie 에 HttpOnly, SameSite 가 없는 것을 확인합니다.
응답에 X-Content-Type-Options, Content-Security-Policy 같은 보안 헤더가 없고 Set-Cookie 에 HttpOnly, SameSite 가 없는 것을 확인합니다.

2. baseline 스캔 돌리기

baseline 스캔은 공식 문서 기준으로 대상 주소를 spider 로 최대 1분(-m 기본값) 돌며 페이지를 모으고, 그 응답에 수동(passive) 규칙만 적용합니다. 공격 요청을 만들어 보내는 능동 스캔(active scan)은 하지 않으므로 응답 헤더, 쿠키, 페이지 내용을 보는 점검입니다. 따라서 SQL 인젝션이나 XSS 같은 취약점은 이 스캔으로 찾지 못합니다.

같은 도커 네트워크에서 컨테이너 이름으로 접속합니다. 보고서를 파일로 받으려면 wrk 폴더를 /zap/wrk 에 마운트해야 합니다.

docker run --rm --network cr-zap-net -v "$(pwd -W)/wrk:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://cr-zap-app:5000 -r report1.html | grep -E '^(WARN|FAIL)'; echo exit=${PIPESTATUS[0]}

Git Bash 에서는 호스트 쪽 경로를 $(pwd -W) 로 써서 C:/... 형태로 넘깁니다. Linux 와 macOS 에서는 $(pwd) 를 씁니다. 기대 출력입니다. 건수는 spider 가 찾은 페이지에 따라 실행마다 조금 달라질 수 있습니다.

WARN-NEW: Cookie No HttpOnly Flag [10010] x 2
WARN-NEW: Missing Anti-clickjacking Header [10020] x 4
WARN-NEW: X-Content-Type-Options Header Missing [10021] x 4
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 5
WARN-NEW: Storable and Cacheable Content [10049] x 5
WARN-NEW: Cookie without SameSite Attribute [10054] x 2
WARN-NEW: Permissions Policy Header Not Set [10063] x 5
WARN-NEW: Session Management Response Identified [10112] x 3
WARN-NEW: Cross-Origin-Embedder-Policy Header Missing or Invalid [90004] x 9
FAIL-NEW: 0   FAIL-INPROG: 0   WARN-NEW: 9   WARN-INPROG: 0   INFO: 0   IGNORE: 0   PASS: 58
exit=2

WARN-NEW 아홉 줄과 요약 줄(FAIL-NEW 0, WARN-NEW 9, PASS 58), 마지막 exit=2가 보이는 것을 확인합니다.
WARN-NEW 아홉 줄과 요약 줄(FAIL-NEW 0, WARN-NEW 9, PASS 58), 마지막 exit=2가 보이는 것을 확인합니다.

읽는 법입니다. 대괄호 숫자가 규칙 ID 이고 뒤의 x N 은 해당 경고가 발견된 URL 수입니다. PASS 는 규칙을 돌렸는데 문제가 없었다는 뜻이고, WARN-NEW 는 이번에 처음 나온 경고입니다. FAIL-NEW 는 설정 파일에서 FAIL 로 지정한 규칙이 걸린 경우에만 나옵니다. 설정 파일이 없으면 모든 경고가 WARN 으로 취급된다고 공식 문서에 적혀 있습니다. 종료 코드는 0 이 성공, 1 이 FAIL 있음, 2 가 WARN 만 있음, 3 이 그 밖의 실패입니다. 위 결과는 WARN 만 있어서 2 입니다.

콘솔 요약과 보고서는 세는 단위가 다릅니다. 콘솔은 규칙 ID 단위라 9건이고, 보고서의 Alerts 표는 변형 단위라 11건(Medium 2, Low 7, Informational 2)입니다. 규칙 90004(공식 이름 Insufficient Site Isolation Against Spectre Vulnerability) 하나가 Cross-Origin-Resource-Policy(90004-1), Cross-Origin-Embedder-Policy(90004-2), Cross-Origin-Opener-Policy(90004-3) 세 건으로 나뉘기 때문입니다. 콘솔에는 그중 이름 하나(Embedder)만 찍히고 x 9 는 세 변형을 합친 URL 수입니다.

3. HTML 보고서에서 경고 목록 읽기

wrk/report1.html 이 만들어졌습니다. 이 폴더에는 zap.yaml 도 생기는데, ZAP 이 내부에서 쓴 자동화 계획 파일입니다.

보고서의 Alerts 표에 CSP 미설정과 Anti-clickjacking 헤더 누락이 Medium, 쿠키 속성과 나머지 헤더 항목이 Low로 나열된 것을 확인합니다.
보고서의 Alerts 표에 CSP 미설정과 Anti-clickjacking 헤더 누락이 Medium, 쿠키 속성과 나머지 헤더 항목이 Low로 나열된 것을 확인합니다.

Alerts 표에서 위험도가 높은 것부터 봅니다. 이 앱에서는 CSP 미설정과 Anti-clickjacking 헤더 누락이 Medium 이고, 쿠키 속성 두 건, X-Content-Type-Options, Permissions-Policy, 그리고 Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy 세 건이 Low 입니다. 항목 이름을 누르면 아래 Alert Detail 에 해당 URL, 응답에서 근거가 된 부분, 수정 방법(Solution)이 나옵니다. Session Management Response Identified 와 Storable and Cacheable Content 는 Informational 이라 문제라기보다 참고 정보입니다.

4. 경고별로 앱 고치기

각 경고에 대응하는 수정은 다음과 같습니다.

규칙 ID 수정
10010, 10054 쿠키에 httponly=True, samesite="Lax"
10021 X-Content-Type-Options: nosniff
10020, 10038 CSP 의 frame-ancestors 'none' 과 X-Frame-Options: DENY, default-src 'self'
10063 Permissions-Policy
90004 Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy, Cross-Origin-Resource-Policy
10049 Cache-Control: no-store (실습용으로 모든 응답의 캐시를 막습니다)

app.py 를 아래 내용으로 바꿔 씁니다. 이 앱은 /memo 가 입력값을 그대로 출력하므로 escape 도 함께 적용했습니다. baseline 은 이 문제를 찾지 못하지만 같은 앱을 고치는 김에 막아 둡니다.

cat > app/app.py <<'PY'
from flask import Flask, make_response, request
from markupsafe import escape

app = Flask(__name__)


@app.get("/")
def index():
    resp = make_response(
        "<html><head><title>메모 앱</title></head><body>"
        "<h1>메모 앱</h1><p><a href='/login'>로그인</a> | <a href='/memo?q=hello'>메모 검색</a></p>"
        "</body></html>"
    )
    resp.set_cookie("session", "demo-session-value", httponly=True, samesite="Lax")
    return resp


@app.get("/login")
def login():
    return "<html><head><title>로그인</title></head><body><h1>로그인</h1><a href='/'>처음으로</a></body></html>"


@app.get("/memo")
def memo():
    q = request.args.get("q", "")
    return f"<html><head><title>메모</title></head><body><h1>검색어</h1><p>{escape(q)}</p><a href='/'>처음으로</a></body></html>"


@app.after_request
def add_security_headers(resp):
    resp.headers["X-Content-Type-Options"] = "nosniff"
    resp.headers["X-Frame-Options"] = "DENY"
    resp.headers["Content-Security-Policy"] = "default-src 'self'; frame-ancestors 'none'; form-action 'self'"
    resp.headers["Permissions-Policy"] = "geolocation=(), camera=(), microphone=()"
    resp.headers["Cache-Control"] = "no-store"
    resp.headers["Cross-Origin-Opener-Policy"] = "same-origin"
    resp.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
    resp.headers["Cross-Origin-Resource-Policy"] = "same-origin"
    return resp
PY

form-action 'self' 는 처음 고친 뒤 재스캔에서 CSP: Failure to Define Directive with No Fallback [10055] 가 나와 추가했습니다. 이 디렉티브는 default-src 로 대체되지 않아 따로 적어야 합니다. 이 헤더들은 부작용이 있으므로 그대로 복사해 서비스에 넣지 않습니다. Cross-Origin-Embedder-Policy: require-corp 는 CORS 나 Cross-Origin-Resource-Policy 로 허용되지 않은 외부 리소스(CDN 이미지, 스크립트, 서드파티 임베드)를 불러오지 못하게 막고, Cross-Origin-Resource-Policy: same-origin 은 다른 출처의 페이지가 내 리소스를 가져가는 것을 막습니다. Cross-Origin-Opener-Policy: same-origin 은 다른 출처 팝업과의 window.opener 연결을 끊어서 팝업 방식의 OAuth 로그인이나 결제창이 완료 신호를 받지 못하게 할 수 있습니다. MDN 은 신뢰하는 팝업을 유지해야 하면 same-origin-allow-popups 를 쓰라고 안내합니다. 이 앱은 외부 리소스도 팝업도 없어서 문제가 없었습니다. 외부 리소스나 팝업이 있는 앱은 한 번에 적용하지 않고, COEP 는 위반만 보고하는 Cross-Origin-Embedder-Policy-Report-Only 헤더로 먼저 확인하고, 나머지도 화면을 직접 눌러 보고 적용합니다. 근거: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy

Cache-Control: no-store 도 모든 응답에 붙이면 정적 파일까지 매번 서버에서 다시 받아 성능과 트래픽이 늘어납니다. 이 앱은 로그인 기능이 없고 쿠키 값도 연습용이라 전체에 붙여 경고를 없앴지만, 실제 서비스에서는 개인 정보나 세션이 담긴 응답에만 붙이고 정적 파일에는 별도 캐시 정책을 두는 편이 낫습니다.

서버 버전 노출(Server 헤더)은 gunicorn 이 gunicorn 이라는 이름만 보내서 이번 스캔에서는 경고가 나오지 않았습니다. 앱이나 프록시에서 Server: nginx/1.x.x 처럼 버전이 붙어 나간다면 그 줄을 제거하는 것이 같은 종류의 수정입니다.

5. 다시 스캔해서 경고가 줄었는지 확인하기

앱을 새 이미지로 교체하고 같은 명령으로 스캔합니다.

docker rm -f cr-zap-app
docker build -t cr-zap-app:v2 app
docker run -d --name cr-zap-app --network cr-zap-net -p 127.0.0.1:20200:5000 cr-zap-app:v2
curl -si http://127.0.0.1:20200/ | head -15
docker run --rm --network cr-zap-net -v "$(pwd -W)/wrk:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://cr-zap-app:5000 -r report2.html | grep -E '^(WARN|FAIL)'; echo exit=${PIPESTATUS[0]}

헤더가 모두 붙은 응답과, 남은 경고가 Non-Storable Content와 Session Management Response Identified 두 건뿐이고 exit=2인 것을 확인합니다.
헤더가 모두 붙은 응답과, 남은 경고가 Non-Storable Content와 Session Management Response Identified 두 건뿐이고 exit=2인 것을 확인합니다.

콘솔 요약 기준으로 아홉 건이던 경고가 두 건으로 줄었고, 보고서의 Alerts 표로는 11건에서 2건이 되었습니다. 남은 두 건은 Informational 입니다. Storable and Cacheable Content 는 no-store 를 붙이자 Non-Storable Content 로 이름이 바뀌어 다시 나왔는데, 캐시되지 않는다는 사실을 알려 주는 항목입니다. 두 건 모두 이 앱에서는 의도한 상태이므로 다음 단계에서 무시 처리합니다.

쿠키의 Secure 속성은 이 글에서 다루지 못했습니다. 실습 앱이 HTTP 로만 열려 있어서 ZAP 이 이 규칙(10011)을 HTTP 응답에는 적용하지 않았기 때문입니다. HTTPS 로 서비스하는 앱이라면 set_cookie(..., secure=True) 를 더합니다.

경고 0건은 응답 헤더와 쿠키 수준의 점검을 통과했다는 뜻이고 앱이 안전하다는 뜻이 아닙니다. 인젝션 같은 취약점은 능동 스캔이나 코드 리뷰로 따로 봅니다. 또 baseline 은 spider 가 링크로 찾은 페이지만 보므로 로그인 이후 화면이나 링크로 이어지지 않는 경로는 점검하지 못합니다.

6. 규칙 설정 파일과 종료 코드로 CI 게이트 만들기

설정 파일은 탭으로 구분한 규칙ID<탭>동작<탭>(이름) 형식이고, 동작은 IGNORE, WARN, FAIL 중 하나입니다. 헤더와 쿠키 관련 다섯 규칙은 FAIL 로, 참고용 두 규칙은 IGNORE 로 둡니다. 파일에 없는 규칙은 기본인 WARN 입니다. 설정 파일은 마운트한 wrk 폴더에 두어야 컨테이너가 읽습니다. -c zap-rules.tsv 는 컨테이너 안의 /zap/wrk 에서 찾기 때문입니다.

printf '# cr-zap baseline rules\n10010\tFAIL\t(Cookie No HttpOnly Flag)\n10020\tFAIL\t(Missing Anti-clickjacking Header)\n10021\tFAIL\t(X-Content-Type-Options Header Missing)\n10038\tFAIL\t(Content Security Policy (CSP) Header Not Set)\n10054\tFAIL\t(Cookie without SameSite Attribute)\n10049\tIGNORE\t(Storable and Cacheable Content)\n10112\tIGNORE\t(Session Management Response Identified)\n' > wrk/zap-rules.tsv

수정한 앱(v2)으로 돌리면 FAIL 이 없어서 종료 코드가 0 입니다. 처음 앱(v1)으로 같은 설정을 돌리면 1 이 나옵니다. 앞의 세 번은 수정 전 앱을 띄운 상태에서 실행하므로, 5절에서 v2 로 교체했다면 먼저 v1 으로 되돌립니다.

# v1 (수정 전 앱)
docker rm -f cr-zap-app; docker run -d --name cr-zap-app --network cr-zap-net -p 127.0.0.1:20200:5000 cr-zap-app:v1
docker run --rm --network cr-zap-net -v "$(pwd -W)/wrk:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://cr-zap-app:5000 -c zap-rules.tsv > /dev/null; echo exit=$?
docker run --rm --network cr-zap-net -v "$(pwd -W)/wrk:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://cr-zap-app:5000 -c zap-rules.tsv -I > /dev/null; echo exit=$?
docker run --rm --network cr-zap-net -v "$(pwd -W)/wrk:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://cr-zap-app:5000 -I > /dev/null; echo exit=$?

마지막 한 번은 앱을 v2 로 교체한 뒤 실행합니다.

# v2 (수정한 앱으로 교체한 뒤)
docker rm -f cr-zap-app; docker run -d --name cr-zap-app --network cr-zap-net -p 127.0.0.1:20200:5000 cr-zap-app:v2
docker run --rm --network cr-zap-net -v "$(pwd -W)/wrk:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://cr-zap-app:5000 -c zap-rules.tsv > /dev/null; echo exit=$?

기대 출력은 순서대로 exit=1, exit=1, exit=0, exit=0 입니다. 앞의 세 줄은 v1 앱, 마지막 한 줄은 v2 앱을 대상으로 한 결과입니다.

처음 두 번은 FAIL 규칙 때문에 exit=1, 설정 파일 없이 -I를 준 세 번째는 exit=0, 수정한 앱에 설정 파일을 적용한 마지막은 exit=0인 것을 확인합니다.
처음 두 번은 FAIL 규칙 때문에 exit=1, 설정 파일 없이 -I를 준 세 번째는 exit=0, 수정한 앱에 설정 파일을 적용한 마지막은 exit=0인 것을 확인합니다.

-I 는 경고(WARN)가 있어도 실패로 보지 않는 옵션입니다. 그래서 두 번째처럼 설정 파일의 FAIL 이 걸리면 -I 를 줘도 1 이 나옵니다. CI 에서는 -c 로 막을 규칙만 FAIL 로 정하고 나머지 경고는 보고서로 남기는 구성이 현실적입니다. 파이프라인 스텝은 종료 코드가 0 이 아니면 실패하므로 명령을 그대로 넣으면 됩니다. 스캔 대상은 파이프라인 안에서 띄운 테스트용 앱이어야 합니다. 아래 ci-net 과 app 은 예시로 가정한 이름이고, 스테이징 같은 다른 환경을 스캔할 때도 그 환경 소유자의 허가가 필요합니다.

docker run --rm --network ci-net -v "$PWD/wrk:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://app:5000 -c zap-rules.tsv -r report.html

자주 막히는 곳

FAIL 이 없는데 종료 코드가 2 라서 파이프라인이 실패합니다. WARN 이 하나라도 있으면 2 입니다. 막지 않을 경고는 설정 파일에서 IGNORE 로 바꾸거나 -I 를 붙입니다. 종료 코드 3 은 ZAP 이 시작하지 못했거나 대상에 연결하지 못한 경우이므로 경고와 구분해서 봅니다.

-t 에 127.0.0.1 을 넣었더니 스캔이 대상에 닿지 않습니다. ZAP 컨테이너 안의 127.0.0.1 은 ZAP 자신이라 Connection refused 로 실패하고 Job spider failed to access URL 이 출력됩니다. 같은 도커 네트워크에 두고 컨테이너 이름(http://cr-zap-app:5000)으로 접속합니다.

Linux 에서 보고서가 만들어지지 않습니다. 컨테이너의 zap 사용자(uid 1000)가 wrk 폴더에 쓸 수 있어야 하므로 chmod a+w wrk 를 해 둡니다. 이 환경(Windows)에서는 재현되지 않아 Linux 에서 직접 확인하지는 못했습니다.

처음 고친 뒤에도 CSP 경고가 남습니다. default-src 만으로는 form-action 이 대체되지 않아 10055 가 나왔습니다. 위 4번 코드처럼 form-action 'self' 를 함께 적습니다.

이어서 해 볼 것

  • 보고서 형식 늘리기: -J report.json(JSON), -w report.md(Markdown), -x report.xml(XML) 옵션을 더해 CI 산출물로 보관합니다.
  • 스캔 시간 조정: -m 으로 spider 시간을 늘리고 앱에 페이지가 더 많을 때 경고 건수가 어떻게 달라지는지 봅니다.
  • FAIL 규칙 넓히기: -g gen.conf 로 기본 설정 파일을 만들면 전체 규칙이 WARN 으로 나열됩니다. 팀 기준에 맞춰 규칙을 하나씩 FAIL 이나 IGNORE 로 바꿉니다.

확인 범위

직접 실행한 것은 1번부터 6번의 모든 명령이며, 환경은 Windows 11 의 Git Bash, Docker Desktop 29.5.3, ZAP 2.17.0 입니다. 옵션과 종료 코드, 설정 파일 형식은 공식 문서(https://www.zaproxy.org/docs/docker/baseline-scan/)로, 이미지 이름과 태그, Docker Hub 병행 이미지는 https://www.zaproxy.org/docs/docker/about/ 로, 규칙 90004 의 세 변형 이름은 https://www.zaproxy.org/docs/alerts/90004/ 로 확인했습니다.

Docker Hub 의 zaproxy/zap-stable 이미지, 보안 헤더 부작용(외부 리소스 차단, 팝업 단절)의 실제 재현은 하지 않았고 MDN 문서 내용을 근거로 적었습니다. Linux 와 macOS 의 $(pwd) 명령, Linux 호스트에서 wrk 폴더에 컨테이너의 zap 사용자(uid 1000)가 쓸 수 있는지는 확인하지 못했습니다. Docker Desktop 의 Windows 에서는 별도 권한 설정 없이 보고서가 만들어졌습니다. MSYS_NO_PATHCONV=1 지정 여부에 따른 차이는 이 환경에서 나타나지 않았고, 변환이 실제로 문제가 되는 경우는 재현하지 못했습니다. HTTPS 앱의 쿠키 Secure 규칙과 Server 헤더 버전 노출 규칙은 이 앱에서 경고가 나오지 않아 확인하지 못했습니다. GitHub Actions 나 Jenkins 에서의 실행도 해 보지 않았습니다. 마지막 CI 예시의 ci-net 과 app 은 가정한 이름입니다.

이 글의 다음 단계

  • Checkov로 Terraform 설정 오류를 찾고 고치기연습용 main.tf를 apply 없이 Checkov로 점검해 실패 항목 표를 읽고, 설정을 고쳐 통과시킵니다. 사유를 남기는 skip 주석, soft-fail과 종료 코드로 만드는 CI 게이트, json과 sarif 출력, Trivy config와의 결과 비교까지 다룹니다.
  • gitleaks로 저장소의 시크릿을 찾고 커밋 전에 막기임시 git 저장소에 가짜 키를 넣고 gitleaks로 커밋 이력을 스캔합니다. JSON과 SARIF 리포트, baseline, 오탐 제외, pre-commit 훅까지 차례로 설정하고 GitHub Actions 예시를 붙입니다.

DevSecOps 실무 과정의 일부입니다

이 실습은 교육에서 참가자마다 독립된 환경으로 진행합니다. 원하는 일정을 고르면 문의 양식에 미리 채워집니다.

과정 보기

교육 진행 순서와 자주 묻는 질문 보기

같은 카테고리의 글