본문으로 건너뛰기

기계 번역입니다. 정식 기준은 영어판이며, 원어민 검수는 아직 완료되지 않았습니다.

audit

검증인이 오프라인으로 확인할 수 있는 감사 증거

작성자 Olivares AI 9 분 소요

감사자는 AI 에이전트가 지난 분기에 수행한 작업에 대한 증거를 요청합니다. 감사 로그의 CSV 내보내기를 그들에게 건네줍니다. 그들은 한 가지 질문을 합니다. “이 글이 사실 이후에 편집되지 않았다는 것을 증명할 수 있습니까?” 대부분의 팀은 그렇지 않습니다. 로그는 로그를 작성한 동일한 시스템에서 내보낸 변경 가능한 데이터베이스에 있습니다. 감사인은 믿음에 따라 모든 과정을 밟아야 하는데, 이는 바로 그것이 증거가 되지 못하게 만드는 특성입니다.

이 게시물에서는 플랫폼이 믿음 없이 유지되는 감사 증거를 구성하는 방법을 설명합니다. 이전 게시물에서는 Claude Code 및 MCP 배포에 에이전트별 ID와 변조 방지 원장이 중요한 이유를 다루었습니다. 이것은 증거를 독립적으로 검증 가능하게 만드는 세 가지 속성, 즉 해시 체인, 이벤트별 암호화 서명, 감사 도구가 이미 이해하고 있는 기계 판독 가능 규정 준수 형식으로 내보내기에 대해 더 자세히 설명합니다.

해시 체인: 모든 이벤트는 모든 이전 이벤트에 커밋됩니다.

원장은 추가 전용이며 테넌트별로 해시 체인으로 연결됩니다. 각 이벤트는 prev_hash 필드에 이전 이벤트의 SHA-256 해시를 전달합니다. 이벤트 N의 체인 해시는 이벤트 N-1의 prev_hash와 연결된 이벤트 N의 자체 필드(테넌트, 시퀀스 번호, 타임스탬프, 행위자, 작업, 대상, 메타데이터 다이제스트, 페이로드 해시)를 포함하는 정식 이진 사전 이미지를 통해 계산됩니다. 시퀀스 1에서 prev_hash는 모두 0(제네시스 앵커)입니다.

중요한 속성: 체인 중간에 있는 이벤트를 수정, 삽입 또는 삭제하면 해당 해시가 변경되어 다음 이벤트의 prev_hash 링크가 끊어지고 다음 이벤트가 끊어지는 식으로 끝까지 이어집니다. 체인을 탐색하고 각 해시를 다시 계산하면 단일 편집 내용을 감지할 수 있습니다.

사전 이미지는 JSON이 아닌 버전이 지정된 도메인 구분 기호가 있는 고정된 길이 접두사 바이너리 인코딩입니다. 이는 JSON 기반 해시가 열어 두는 세 가지 공격 표면에 대한 의도적인 선택입니다.

  • 키 순서: JSON 객체 키 순서는 대부분의 직렬 변환기에서 보장되지 않습니다. 키 순서가 다르면 의미상 동일한 데이터에 대해서도 다른 해시가 생성되어 잘못된 체인 중단이 발생하거나 더 나쁜 경우 공격자가 키 순서를 변경하여 일치하는 해시를 만들 수 있습니다.
  • 공백 및 숫자 형식: {"seq": 1}{"seq":1}{"seq": 1.0}는 의미상 동일한 JSON이지만 다른 SHA-256 다이제스트를 생성합니다.
  • 연결 위조: 길이 접두사가 없으면 두 개의 인접한 짧은 필드를 병합하여 동일한 해시를 가진 세 번째 긴 필드를 위조할 수 있습니다. 각 필드의 길이 앞에 해당 바이트 수(4바이트 빅엔디안)를 추가하면 이 필드가 닫힙니다.

core/internal/store/canon/canon.go의 구현은 단일 정보 소스입니다. Append(쓰기)와 Verify(다시 읽기)는 모두 동일한 EventHash 함수를 호출합니다. 드리프트에 대한 두 번째 구현은 없습니다.

func EventHash(e Event) []byte {
    var buf []byte
    buf = lps(buf, domainEvent)       // "olivares.audit.v1"
    buf = lps(buf, e.TenantID)
    var seq [8]byte
    binary.BigEndian.PutUint64(seq[:], uint64(e.Seq))
    buf = append(buf, seq[:]...)
    buf = lps(buf, e.OccurredAt)
    buf = lps(buf, e.Actor)
    buf = lps(buf, e.ActorKind)
    buf = lps(buf, e.Action)
    buf = lps(buf, e.TargetKind)
    buf = lps(buf, e.TargetID)
    buf = append(buf, fixed(e.MetaDigest)...)
    buf = append(buf, fixed(e.PayloadHash)...)
    buf = append(buf, fixed(e.PrevHash)...)
    sum := sha256.Sum256(buf)
    return sum[:]
}

도메인 구분 기호 "olivares.audit.v1"는 해시를 해당 목적 및 버전에 바인딩합니다. 다른 도메인의 해시(체크포인트, 페이로드, 메타데이터 다이제스트)는 원시 바이트가 일치하더라도 이벤트 해시와 절대 충돌할 수 없습니다.

Ed25519 서명: 모든 이벤트는 자체 앵커입니다.

해시 체인은 내부 일관성을 입증하지만 신뢰성은 입증하지 않습니다. 원시 데이터베이스 쓰기 액세스 권한을 가진 공격자는 변경된 이벤트를 사용하여 전체 체인을 처음부터 다시 계산하고 원본과 다르지만 내부적으로는 일관된 유효한 체인을 생성할 수 있습니다. 해시 체인은 변조를 감지합니다. 원산지를 증명하지 않습니다.

이벤트별 Ed25519 서명으로 이 gap가 닫힙니다. 원장에 추가된 모든 이벤트는 쓰기 시 서명됩니다. 서명은 도메인으로 구분된 테넌트의 사전 이미지, 시퀀스 번호 및 이벤트의 체인 해시를 포함합니다.

domain ("olivares.audit.event.v1") || tenant || seq (8 bytes, big-endian) || hash

서명은 이벤트에 저장되지만 설계상 체인 해시 사전 이미지에서는 제외됩니다. 이는 우연이 아닙니다. 서명이 해시에 포함된 경우 이벤트에 서명하면 증명해야 하는 해시가 변경됩니다. 서명은 해시를 변경하지 않고 해시를 증명합니다.

공개 키만 보유한 외부 검증자는 각 이벤트를 개별적으로 확인할 수 있습니다. 즉, 이벤트 필드에서 체인 해시를 다시 계산하고, 사전 이미지를 재구성하고, Ed25519 서명을 확인할 수 있습니다. 서명 후 이벤트가 변경된 경우 해당 특정 이벤트에 대한 서명 확인이 실패합니다. 검증자는 증거를 생성한 시스템을 신뢰할 필요가 없습니다.

플랫폼은 키 순환도 지원합니다. 수명 중간에 서명 키가 변경된 체인은 현재 키와 이전 세대의 공개 키를 고정하여 엔드투엔드를 확인합니다. 확인 기능은 후보 키 세트를 수락하고 후보가 이를 확인하면 이벤트가 유효한 것으로 간주합니다.

이벤트별 서명 외에도 정기적인 체크포인트는 별도의 서명 도메인(olivares.audit.checkpoint.v1)에서 체인 팁을 공증합니다. 데이터베이스 수준뿐만 아니라 호스트 수준 손상을 방어해야 하는 조직의 경우 개인 키가 호스트에 존재하지 않는 오프박스 KMS/HSM 키(AWS KMS, GCP Cloud KMS, Azure Key Vault)로 체크포인트에 서명할 수 있습니다. 이벤트별 서명은 DB 전용 공격자를 처리합니다. 오프박스 체크포인트는 호스트 침해 공격자를 처리합니다. 두 가지 위협 모델은 서로 다릅니다. 어떤 서명도 둘 다 포함하지 않습니다.

원장 계약: 동일한 거래에 봉인됨

감사 시스템의 일반적인 실패는 상태 변이와 감사 기록 간의 최종 일관성입니다. 상태가 변경되고, 감사 쓰기가 대기열에 추가되거나 일괄 처리되며, 감사 쓰기가 실패하면 상태 변경이 이미 커밋된 것입니다. 그 결과 시스템에는 존재하지만 증거에는 존재하지 않는 감사되지 않은 돌연변이가 발생합니다.

플랫폼은 더 강력한 계약을 시행합니다. 상태 변이와 원장 봉인은 모두 동일한 데이터베이스 트랜잭션에서 발생합니다. 봉인이 실패하면 전체 전환이 롤백됩니다. 즉, 상태 변이는 커밋되지 않습니다. 이는 최선의 노력이 아닙니다. 거부되어 닫혀 있습니다.

세션 런타임 원장이 이를 보여줍니다. Claude Code 세션이 상태(생성됨, 시작됨, 중지 중, 중지됨, 실패함)를 전환하면 appendRunEvent는 전환을 원자적으로 두 위치에 기록합니다.

  1. sc.Audit().Append를 통한 글로벌 해시 체인 감사 원장(PayloadHash에 의해 고정된 변조 방지 체인)
  2. 세션별 쿼리 가능 원장audit_seq에 의해 글로벌 체인에 연결된 추가 전용 프로젝션입니다.

두 쓰기 모두 호출자의 Mutate 트랜잭션 내에서 발생합니다. runtime_ledger.go의 코드 주석에는 설계 의도가 직접적으로 명시되어 있습니다. “원장은 기록 시스템이므로 봉인이 실패하면 전체 전환이 롤백됩니다. 이는 최선의 노력이 아닙니다.”

PayloadHash 자체는 실행 참조, 시퀀스, 이벤트 유형, 상태 전환 및 타임스탬프와 같은 표준적이고 민감하지 않은 전환 사실에만 커밋됩니다. 여기에는 기록 콘텐츠, 프롬프트, 환경 값 또는 비밀이 포함되지 않습니다. 원장은 무슨 일이 일어났는지 증명합니다. 말한 내용은 저장되지 않습니다.

workspace_ledger.go의 작업공간 파일 변형에도 동일한 패턴이 적용됩니다. 파일 쓰기, mkdir, 이동 또는 삭제는 파일 시스템 작업이 실행되기 전에 봉인됩니다. 증거를 추가할 수 없으면 변형이 실행되지 않습니다. 봉인은 작성된 콘텐츠의 작업 유형, 경로 및 SHA-256를 전달하며 콘텐츠 바이트 자체는 전달하지 않습니다.

OSCAL 내보내기: 감사자의 도구 수집을 기계로 읽을 수 있는 증거

변조 방지 원장이 필요하지만 감사자에게는 충분하지 않습니다. 증거가 독점 형식인 경우에도 감사인은 이를 해석하기 위해 여전히 귀하의 도구에 의존합니다. OSCAL(NIST에서 유지 관리하는 개방형 보안 제어 평가 언어)는 이 gap를 닫는 형식입니다.

플랫폼은 봉인된 증거 패키지를 세 가지 모델이 포함된 OSCAL 번들로 내보냅니다.

  • 구성요소 정의: 규정 준수 프레임워크(NIST SP 800-53, ISO 27001, EU AI Act 등)에 따라 구현된 요구 사항으로 표현되는 제어 플레인의 기능입니다. 구현된 각 요구 사항에는 컨트롤 ID, 이를 증명하는 기능 키 및 사용자 정의 속성으로서의 실제 상태가 포함됩니다.
  • 평가 결과: OSCAL 준수 상태의 컨트롤별 결과. 각 결과의 대상에는 이유 필드에 보존된 정확한 제품 상태와 함께 satisfied 또는 not-satisfied가 포함됩니다.
  • 컨트롤 매핑: OSCAL 1.2.0 컨트롤 매핑 모델을 사용하여 프레임워크 컨트롤에서 플랫폼 기능 참조 모델까지의 횡단보도입니다. 관계는 항상 intersects-with입니다. 즉, 기능은 컨트롤의 일부를 처리합니다. 이는 결코 적합성을 주장하지 않습니다. 그 주장은 실제 운영 증거를 바탕으로 한 평가 결과에만 존재합니다.

OSCAL 내보내기의 정직성 제약 조건은 명시적으로 언급할 가치가 있습니다. OSCAL의 발견 상태 열거형에는 satisfiednot-satisfied의 두 가지 값이 있습니다. “partial” 또는 “설계상”은 없습니다. partially 구현되고 설계에 따라 처리되는 컨트롤인 gapped 또는 unmapped는 OSCAL not-satisfied에 매핑되며 실제 제품 상태는 status.reason에 탑재되고 플랫폼 자체 네임스페이스(https://olivares.ai/ns/oscal) 아래의 사용자 지정 속성이 포함됩니다. 내보내기에서는 partially-met 컨트롤을 satisfied로 세탁하지 않습니다. 봉인된 실시간 운영 증거로 뒷받침되는 컨트롤만 OSCAL satisfied를 받습니다.

모든 OSCAL 문서에는 원장 앵커 속성(매니페스트 해시, 봉인 시 원장 시퀀스 번호, 원장 해시 및 무결성 확인 결과)이 포함되어 있습니다. 이는 감사자가 GRC 도구에서 읽는 OSCAL 문서와 독립적으로 확인할 수 있는 기본 변조 방지 체인 사이의 다리입니다.

증거 파이프라인 활동

오프라인 인증의 구체적인 의미

‘오프라인 인증’은 마케팅 문구가 아닙니다. 이는 특정 기술 절차를 설명합니다. 검증자는 내보낸 증거를 가져와 air-gapped 시스템에서 검증 도구를 실행하고 증거를 생성한 시스템에 대한 네트워크 액세스 없이 증거의 무결성을 확인합니다.

플랫폼의 아카이브 내보내기는 JSONL 세그먼트(이벤트당 한 줄, 표준 JSON)의 디렉터리 트리와 세그먼트당 매니페스트를 작성합니다. 매니페스트는 세그먼트의 시퀀스 범위, 이벤트 수, 첫 번째 및 마지막 체인 해시, 이벤트 파일의 SHA-256, 세그먼트 간 연속성을 위한 이전 세그먼트의 마지막 해시를 기록합니다.

그런 다음 오프라인 검증자(VerifyArchiveDir)는 네트워크 호출 없이 상수 메모리에서 완전히 다음을 수행합니다.

  1. 매니페스트를 로드하고 이벤트 파일과 페어링합니다. 이탈 이벤트 파일(매니페스트 없음) 또는 이벤트 파일이 누락된 매니페스트는 실패입니다. 증거의 단위는 쌍이다.
  2. 각 이벤트 파일을 한 줄씩 스트리밍합니다. 각 이벤트에 대해 라이브 시스템에서 사용하는 것과 동일한 EventHash 기능을 사용하여 보관된 필드에서 체인 해시를 다시 파생합니다. 저장된 해시와 비교하십시오. prev_hash 연결 및 시퀀스 gap-freedom을 확인하세요.
  3. 정규성을 확인합니다. 구문 분석된 각 줄을 다시 정렬하고 디스크 상의 바이트에 대해 바이트와 동일한 출력이 생성되는지 확인합니다. 이는 해시 검사를 통과하지만 숨겨진 데이터를 전달하는 알 수 없는 필드 밀수나 중복 키 공격을 방지합니다.
  4. 이벤트별 Ed25519 서명을 확인합니다. 체크포인트가 아닌 각 이벤트에 대해 서명 사전 이미지를 재구성하고 고정된 공개 키와 비교하여 확인합니다.
  5. 체크포인트 서명을 확인합니다. 각 체크포인트 이벤트에 대해 고정된 체크포인트 키에 대해 체크포인트 도메인 아래의 서명을 확인합니다. 이벤트 키만 고정된 경우(체크포인트 키 없음) 체크포인트 이벤트는 “확인할 수 없음”으로 표시됩니다. 즉, 건너뛸 수 없는 거부 닫힘입니다.
  6. 세그먼트 간 연속성을 확인하세요. 각 세그먼트의 첫 번째 시퀀스가 이전 세그먼트의 마지막 시퀀스에 1을 더한 값을 따르고, 이전 세그먼트의 마지막 해시가 현재 세그먼트의 prev_segment_last_hash와 일치하는지 확인하세요.
  7. 이벤트 파일 다이제스트를 확인하세요. 스트리밍 중에 계산된 이벤트 파일의 SHA-256는 매니페스트의 events_sha256와 일치해야 합니다.

검증자는 발견한 첫 번째 불일치를 특정 시퀀스 번호와 기계가 읽을 수 있는 이유(hash-mismatch, prev-mismatch, seq-gap, event-sig-invalid, event-sig-missing, checkpoint-sig-invalid, count-mismatch)와 함께 보고합니다. events-sha256-mismatch 또는 segment-link-mismatch.

정직한 제한 사항: 오프라인 검증자는 확인한 범위를 정확하게 증명하고 그 범위 밖에는 아무것도 증명하지 않습니다. 제거된 접두사 또는 꼬리는 오프라인에서 감지할 수 없습니다. 디렉터리는 체인이 시작되거나 끝난 위치를 알려주지 않습니다. 확인 보고서에는 StartsMidChain 플래그가 있는 테넌트별 Ranges 필드가 포함되어 있으므로 감사자는 증명된 내용을 정확히 알 수 있습니다. 라이브 시스템의 체인 내 서명된 체크포인트는 꼬리를 덮습니다. 오프라인 내보내기에는 보관된 범위가 포함됩니다. 그들은 함께 완전한 증명을 형성합니다.

레이어그것이 증명하는 것증명하지 못하는 것
해시체인내부 일관성; 편집하면 체인이 끊어집니다.출처(이벤트를 작성한 사람)
이벤트별 Ed25519원산지; 각 이벤트는 키 보유자가 서명했습니다.호스트 수준 침해로부터 방어
오프박스 체크포인트호스트 손상 저항(호스트에 KMS/HSM 키가 없음)이벤트별 세분성(체크포인트만 포함)
OSCAL 내보내기기계 판독 가능, 프레임워크 매핑된 규정 준수 증거모든 통제가 완전히 충족됨(실시간 증거만 중요함)
아카이브 확인위의 모든 항목을 오프라인으로 다시 파생내보낸 범위 이전 또는 이후의 이벤트

코드 경로: 상태 돌연변이에서 봉인된 증거까지

Claude Code 세션 상태 변경에서 검증 가능한 증거로의 시퀀스는 세 가지 계층에 닿습니다. runtime_ledger.go에서 appendRunEvent 함수는 SHA-256가 정식 길이 접두사 전환 필드를 해싱하여 PayloadHash를 구성합니다.

func runEventPayloadHash(runRef string, seq int64, event, from, to, detail, atTS string) [32]byte {
    h := sha256.New()
    for _, part := range []string{
        runRef, strconv.FormatInt(seq, 10), event, from, to, detail, atTS,
    } {
        _, _ = h.Write([]byte(strconv.Itoa(len(part))))
        _, _ = h.Write([]byte{':'})
        _, _ = h.Write([]byte(part))
    }
    var sum [32]byte
    copy(sum[:], h.Sum(nil))
    return sum
}

그런 다음 해당 해시는 sc.Audit().Append로 전달되며, 이 함수는 다음 테넌트별 시퀀스 번호를 할당하고, 이전 이벤트의 해시에 연결하고, EventHash를 통해 이 이벤트의 체인 해시를 계산하고, Ed25519로 서명하고, 봉인된 이벤트를 삽입합니다. 이 모든 것이 호출자의 트랜잭션 내에 있습니다.

결과: 트랜잭션이 커밋될 때 세션의 상태 변경과 변조 방지 감사 기록이 모두 지속되거나 모두 롤백됩니다. 상태가 변경되었지만 증거가 변경되지 않은 창은 없습니다.

링크

관련 글

자주 묻는 질문

데이터베이스를 손상시킨 공격자가 위조된 이벤트에 다시 서명할 수 있습니까?

이벤트별 Ed25519 서명은 DB 전용 손상(도난된 백업, 삽입된 행 또는 RLS 우회 역할이 있는 복제본)을 방지합니다. 서명 키는 데이터베이스가 아닌 데이터 디렉터리에 있습니다. 호스트 침해 사례의 경우 플랫폼은 개인 키가 호스트에 존재하지 않는 KMS/HSM(AWS KMS, GCP Cloud KMS, Azure Key Vault)를 통한 오프박스 체크포인트 서명을 지원합니다. 이벤트별 서명은 DB 수준 공격을 중지합니다. 오프박스 체크포인트는 호스트 수준 체크포인트를 중지합니다. 어느 쪽도 두 위협 모델을 모두 다루지 않습니다.

OSCAL 내보내기가 컨트롤을 satisfied로 표시했지만 나중에 운영 증거가 변경되면 어떻게 되나요?

OSCAL 내보내기는 봉인 시 제품 상태를 OSCAL 결과 상태 열거형에 매핑합니다. 그 순간 실제 운영 증거로 뒷받침되는 컨트롤만 OSCAL satisfied를 받습니다. by_design, partial, gap 또는 unmapped 상태의 컨트롤은 이유 필드와 사용자 정의 속성에 정확한 제품 상태가 보존된 상태로 OSCAL not-satisfied에 매핑됩니다. 봉인된 각 증거 패키지는 변경할 수 없으며 타임스탬프가 지정됩니다. 증거가 변경되면 현재 상태를 반영하여 새 패키지가 봉인됩니다. 이전 패키지는 그대로 유지되고 재검증 가능하므로 덮어쓸 수 있는 단일 주장이 아닌 시계열 규정 준수 상태를 생성합니다.

에이전트가 무엇에 접근할 수 있는지 확인하세요

Olivares AI는 당신의 AI 환경을 위한 개방형 자체 호스팅 플랫폼입니다. 자체 인프라에 배포하고 보안 및 플랫폼 팀이 필요로 하는 액세스 맵을 확보하세요.