본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
10장 : 보안과 방어적 운영

감사 로그와 공급망 점검

보안 사고와 운영 장애를 조사하려면 "오류가 났다"보다 누가 언제 어떤 대상을 어떤 결과로 변경했는지가 필요합니다. 감사 이벤트는 개발용 debug 로그와 달리 안정된 필드, 접근 통제, 보존 기간, 위변조 방지 정책을 가져야 합니다.

애플리케이션 코드만 검토해서는 배포 이미지에 포함된 전이 의존성과 OS package를 알 수 없습니다. SBOM으로 구성요소를 기록하고 취약점 정책과 이미지 서명을 release gate에 연결합니다.


감사 기록과 디버그 로그는 목적이 다르다

감사 이벤트에는 인증된 주체, 행위, 대상 식별자, 결과, 시각과 상관관계 ID를 구조화해 기록합니다. 비밀번호·JWT·요청 본문 전체는 감사 목적에 필요하지 않으며 오히려 새로운 유출 경로가 됩니다.

SBOM은 취약점이 없다는 인증서가 아니라 "무엇이 들어 있는가"에 대한 재료 목록입니다. scanner 결과는 배포 시점의 정책으로 판정하고, 새 취약점 정보가 나오면 같은 SBOM을 다시 평가할 수 있어야 합니다.


상태 변경 감사 이벤트를 불변 형식으로 기록한다

먼저 감사 필드를 record로 고정하고 key-value 로그로 남깁니다. 실제 운영에서는 append-only 저장소나 서명된 전송 경로로 보내 애플리케이션 사용자가 기록을 임의 수정하지 못하게 합니다.

src/main/java/com/andongmin/studylog/audit/AuditRecorder.java
package com.andongmin.studylog.audit;

import java.time.Clock;
import java.time.Instant;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class AuditRecorder {
    private static final Logger log = LoggerFactory.getLogger(AuditRecorder.class);
    private final Clock clock;

    public AuditRecorder(Clock clock) {
        this.clock = clock;
    }

    public void record(String actor, String action, UUID target, String result) {
        AuditEvent event = new AuditEvent(Instant.now(clock), actor, action, target, result);
        log.info("audit occurredAt={} actor={} action={} target={} result={}",
                event.occurredAt(), event.actor(), event.action(), event.target(), event.result());
    }

    record AuditEvent(Instant occurredAt, String actor, String action,
            UUID target, String result) {
    }
}
release-security-check.sh
#!/usr/bin/env bash
set -euo pipefail
: "${IMAGE:?set IMAGE to an immutable registry tag}"
: "${COSIGN_PRIVATE_KEY:?set signing key reference}"
./mvnw -q verify
./mvnw -q dependency:tree -DoutputFile=target/dependencies.txt
./mvnw spring-boot:build-image \
  -Dspring-boot.build-image.imageName="$IMAGE"
syft "$IMAGE" -o cyclonedx-json=target/sbom.json
grype "$IMAGE" --fail-on high
docker push "$IMAGE"
cosign sign --yes --key env://COSIGN_PRIVATE_KEY "$IMAGE"
cosign verify --key cosign.pub "$IMAGE"

IMAGE에는 latest가 아니라 commit SHA나 digest로 추적 가능한 registry tag를 사용합니다. 취약점 검사까지 통과한 뒤에만 push·sign 단계가 실행됩니다. 개인 키 원문을 저장소나 명령 인자에 넣지 말고 CI의 keyless 서명 또는 secret reference를 사용합니다.


SBOM·취약점·서명 결과를 release gate로 만든다

release 권한이 있는 CI에서만 registry push와 서명을 실행합니다. 로컬에서는 verify, SBOM 생성, 취약점 검사까지만 수행하고, 외부 상태를 바꾸는 단계는 dry-run이나 별도 job으로 분리하는 편이 안전합니다.

bash release-security-check.sh
Gate 확인 결과
테스트, SBOM 생성, high 이상 취약점 정책, 서명 검증 중 하나라도 실패하면 candidate image를 승격하지 않아야 합니다.

gate는 실패 이유와 예외 절차도 남긴다

취약점 하나로 release가 멈췄을 때 package, 심각도, 수정 가능 버전과 판정 시각을 보존합니다. 업무상 임시 예외가 필요하다면 담당자·만료일·완화책을 코드 리뷰 가능한 정책 파일에 기록하고 영구적인 무시 목록으로 만들지 않습니다.


검증된 artifact와 공급망 증거를 만들었으니 다음 장에서는 그 이미지를 Compose와 Kubernetes에 배치하고 안전하게 되돌리는 절차를 연결합니다.