2SeC SIEM 프로젝트: C 기반 로그 정제 시스템 & IAM 인증 구조

3줄 요약

  1. DVWA 공격 로그를 C 파서로 정제해서 LLM 학습용 JSON으로 변환하는 시스템 만들었다
  2. Logstash → OpenSearch 인증은 IAM 방식으로 가서 비밀번호 관리 문제 해결했다
  3. 단순 SIEM이 아니라 LLM CTI까지 연동되는 전체 파이프라인 구축했다

시작하며

2SeC 팀 프로젝트에서 DVWA 기반 공격 로그를 수집하고 분석하는 SIEM 시스템을 만들었다

단순히 로그 모으는 게 아니라, 이걸 LLM이 학습해서 공격 패턴을 자동으로 분석하는 게 목표였다

그 과정에서 두 가지 큰 고민이 있었다

  1. 대용량 로그를 빠르게 처리하려면? → C 기반 파서 개발
  2. 안전하게 인증하려면? → IAM 기반 인증 구조

전체 아키텍처

[DVWA 공격 시뮬레이션]
         ↓
    [공격 로그 생성]
    (raw text format)
         ↓
[C 로그 파싱 엔진] ← Part 1
         ↓
    [JSON 변환]
    (구조화된 데이터)
         ↓
     [Logstash]
         ↓  (IAM 인증) ← Part 2
    [OpenSearch]
         ↓
   [LLM CTI 분석]

Part 1: C 기반 로그 정제 시스템

왜 C로 만들었나?

성능이 필요했다

Python으로 10만 줄 로그 파싱하면 약 5~10초 걸린다

C로 만들면 1초 안에 끝난다

실제 측정

# Python 버전
time python3 parser.py attack.log output.json
real    0m8.342s

# C 버전
time ./log_parser attack.log output.json
real    0m0.721s

약 11배 차이 난다

나중에 실시간 로그 처리할 때 이 차이가 크다

메모리 관리가 명확하다

malloc, realloc, free 직접 관리하니까 메모리 누수 걱정 없고

언제 얼마나 메모리 쓰는지 정확히 알 수 있다

실무 환경 고려

실제 회사 가면 고성능 로그 처리 시스템은 대부분 C/C++로 되어 있다

지금 배워두면 나중에 도움된다


핵심 기능

1. 로그 파싱 엔진

공격 로그가 이런 형식으로 들어온다

2025-12-12 10:39:15 | SQL_INJECTION | SUCCESS | ' AND SLEEP(3) | 192.168.1.100 | SESSION_ABC123

이걸 파이프(|) 기준으로 쪼개서 구조체에 담는다

지원하는 공격 타입

공격 타입enum 값설명
SQL InjectionATTACK_SQL_INJECTIONDB 쿼리 조작 공격
XSSATTACK_XSS스크립트 삽입 공격
Command InjectionATTACK_COMMAND_INJECTIONOS 명령어 삽입
File InclusionATTACK_FILE_INCLUSIONLFI/RFI 공격
Brute ForceATTACK_BRUTE_FORCE무차별 대입 공격
CSRFATTACK_CSRF요청 위조 공격

공격 타입은 enum으로 관리해서 나중에 switch-case로 처리하기 편하게 만들었다

2. 자동 심각도 계산 로직

단순히 로그만 파싱하는 게 아니라 공격의 위험도를 자동으로 계산한다

심각도 계산 알고리즘

int calculate_severity(LogEntry *entry) {
    int severity = 1;  // 기본 점수
    
    // 공격 성공 여부로 가중치
    if (entry->success) {
        severity += 3;
    }
    
    // 공격 유형별 가중치
    switch(entry->attack_type) {
        case ATTACK_SQL_INJECTION:
        case ATTACK_COMMAND_INJECTION:
            severity += 4;  // 시스템 침투 가능한 공격
            break;
            
        case ATTACK_FILE_INCLUSION:
        case ATTACK_XSS:
            severity += 3;  // 정보 탈취 가능한 공격
            break;
            
        case ATTACK_CSRF:
        case ATTACK_BRUTE_FORCE:
            severity += 2;  // 상대적으로 낮은 위험도
            break;
            
        default:
            severity += 1;
    }
    
    if (severity > 10) severity = 10;  // 최대값 제한
    return severity;
}

점수 기준

점수의미예시
1-3낮음실패한 Brute Force
4-6중간실패한 SQL Injection
7-9높음성공한 XSS
10치명적성공한 Command Injection

왜 이렇게 만들었냐면, 나중에 OpenSearch에서 심각도 기준으로 필터링하거나 알림 보낼 때 유용하기 때문이다

3. 메모리 관리

동적 배열로 로그 엔트리를 관리한다

typedef struct {
    LogEntry *entries;  // 동적 배열
    int count;          // 현재 저장된 개수
    int capacity;       // 현재 배열 크기
} LogCollection;

초기화

LogCollection* init_log_collection(void) {
    LogCollection *collection = malloc(sizeof(LogCollection));
    if (!collection) return NULL;
    
    collection->capacity = 1000;  // 초기 크기
    collection->count = 0;
    collection->entries = malloc(sizeof(LogEntry) * collection->capacity);
    
    return collection;
}

초기 capacity를 1000으로 잡았다

테스트해보니 보통 한 번에 1000~5000개 정도 로그가 들어오더라

자동 확장

int add_log_entry(LogCollection *collection, LogEntry *entry) {
    // 꽉 차면 2배로 확장
    if (collection->count >= collection->capacity) {
        collection->capacity *= 2;
        LogEntry *new_entries = realloc(collection->entries,
                                       sizeof(LogEntry) * collection->capacity);
        if (!new_entries) return 0;
        collection->entries = new_entries;
    }
    
    collection->entries[collection->count++] = *entry;
    return 1;
}

capacity 넘어가면 자동으로 2배씩 늘어난다

realloc 실패하면 0 리턴해서 상위에서 에러 처리하게 만들었다

메모리 해제

void free_log_collection(LogCollection *collection) {
    if (collection) {
        if (collection->entries) {
            free(collection->entries);
        }
        free(collection);
    }
}

NULL 체크 꼭 해야 한다

안 하면 segfault 난다

4. JSON 변환

LLM이 바로 학습할 수 있게 표준화된 JSON 포맷으로 변환한다

입력 로그

2025-12-12 10:39:15 | SQL_INJECTION | SUCCESS | ' AND SLEEP(3) | 192.168.1.100 | SESSION_ABC123

출력 JSON

{
  "timestamp": "2025-12-12 10:39:15",
  "attack_type": "SQL_INJECTION",
  "success": true,
  "payload": "' AND SLEEP(3)",
  "source_ip": "192.168.1.100",
  "session_id": "SESSION_ABC123",
  "severity": 8
}

전체 출력 구조

{
  "total_events": 3,
  "events": [
    {...},
    {...},
    {...}
  ]
}

total_events 필드 넣어서 나중에 로그 개수 확인할 때 편하게 만들었다

5. JSON Escape 처리

페이로드에 특수문자 들어가 있으면 JSON 깨진다

그래서 escape 처리 필수다

void escape_json_string(const char *input, char *output, int max_len) {
    int j = 0;
    for (int i = 0; input[i] && j < max_len - 2; i++) {
        // 큰따옴표와 백슬래시 escape
        if (input[i] == '"' || input[i] == '\\') {
            output[j++] = '\\';
        }
        output[j++] = input[i];
    }
    output[j] = '\0';
}

처리 예시

원본변환 후
' OR "1"="1' OR \"1\"=\"1
C:\windows\system32C:\\windows\\system32
<script>alert("XSS")</script><script>alert(\"XSS\")</script>

이거 안 하면 JSON 파싱 실패한다


구현 세부사항

파일 구조

.
├── log_parser.h      # 헤더 파일 (구조체, 함수 선언)
├── log_parser.c      # 핵심 로직 (파싱, 변환)
└── main.c            # 메인 함수 (입출력 처리)

역할 분리 확실히 했다

나중에 라이브러리로 만들 수도 있게

구조체 정의

LogEntry 구조체

typedef struct {
    char timestamp[MAX_TIMESTAMP_LENGTH];       // 32 bytes
    AttackType attack_type;                     // 4 bytes (enum)
    char attack_type_str[MAX_ATTACK_TYPE_LENGTH]; // 64 bytes
    int success;                                 // 4 bytes
    char payload[MAX_PAYLOAD_LENGTH];           // 2048 bytes
    char source_ip[MAX_IP_LENGTH];              // 64 bytes
    char session_id[MAX_SESSION_LENGTH];        // 128 bytes
    int severity;                                // 4 bytes
} LogEntry;

총 크기: 약 2348 bytes

1000개면 약 2.3 MB

크게 부담 안 된다

파싱 로직

strtok를 사용한 토큰 분리

int parse_log_line(const char *line, LogEntry *entry) {
    char temp_line[MAX_LINE_LENGTH];
    strncpy(temp_line, line, MAX_LINE_LENGTH - 1);
    temp_line[MAX_LINE_LENGTH - 1] = '\0';
    
    // 개행 문자 제거
    char *newline = strchr(temp_line, '\n');
    if (newline) *newline = '\0';
    
    // 파이프 기준으로 분리
    char *token = strtok(temp_line, "|");
    if (!token) return 0;
    
    // 공백 제거
    while (*token == ' ') token++;
    char *end = token + strlen(token) - 1;
    while (end > token && *end == ' ') end--;
    *(end + 1) = '\0';
    
    strncpy(entry->timestamp, token, MAX_TIMESTAMP_LENGTH - 1);
    // ... 이후 필드들도 동일하게 처리
    
    return 1;
}

주의할 점

  1. strtok는 원본 문자열을 수정한다
    → 복사본 만들어서 사용

  2. 공백 처리 꼭 해야 한다
    → 앞뒤 공백 제거 로직 추가

  3. 버퍼 오버플로우 방지
    strncpy 사용하고 null 문자 보장

에러 핸들링

메모리 할당 실패

LogCollection *collection = init_log_collection();
if (!collection) {
    fprintf(stderr, "Error: Failed to initialize log collection\n");
    return 1;
}

파일 열기 실패

FILE *fp = fopen(input_file, "r");
if (!fp) {
    fprintf(stderr, "Error: Cannot open input file '%s'\n", input_file);
    return 1;
}

파싱 실패

if (parse_log_line(line, &entry)) {
    // 성공
    parsed_count++;
} else {
    // 실패
    fprintf(stderr, "Warning: Failed to parse line %d\n", line_number);
    error_count++;
}

에러 나도 프로그램 죽지 않게 만들었다

로그 일부 파싱 실패해도 나머지는 처리한다


사용 방법

빌드

gcc -o log_parser main.c log_parser.c -Wall -O2

컴파일 옵션 설명

옵션의미
-Wall모든 경고 표시
-O2최적화 레벨 2
-o출력 파일명 지정

처음엔 -O3 썼는데 -O2랑 속도 차이 거의 없더라

실행

./log_parser <입력파일> <출력파일>

예시

./log_parser data/raw_logs/attack.log data/parsed_logs/attack.json

실행 결과

Parsing log file: data/raw_logs/attack.log
  [1] Parsed: 2025-12-12 10:39:15 | SQL_INJECTION | SUCCESS
  [2] Parsed: 2025-12-12 10:40:22 | XSS | FAILURE
  [3] Parsed: 2025-12-12 10:41:33 | COMMAND_INJECTION | SUCCESS
  [4] Parsed: 2025-12-12 10:42:15 | BRUTE_FORCE | FAILURE

Parsing complete:
  Total lines: 4
  Successfully parsed: 4
  Errors: 0

Writing JSON output to: data/parsed_logs/attack.json
Successfully wrote 4 events to JSON file

Log parsing engine completed successfully.

실시간으로 파싱 진행 상황 보여준다

나중에 큰 파일 처리할 때 멈춘 건지 아닌지 알 수 있어서 좋다


성능 테스트

결과

로그 개수파싱 시간메모리 사용량
1,0000.01초2.3 MB
10,0000.08초23 MB
100,0000.72초230 MB
1,000,0007.89초2.3 GB

100만 줄도 8초 안에 처리한다

Python 버전은 100만 줄 처리하는 데 약 80초 걸렸다

확장 횟수

초기 capacity: 1000

  • 1,000개: 확장 0회
  • 10,000개: 확장 4회 (1000 → 2000 → 4000 → 8000 → 16000)
  • 100,000개: 확장 7회

realloc 오버헤드가 있긴 한데 전체 시간에서 차지하는 비율은 5% 미만이다


어려웠던 점

1. 메모리 누수 디버깅

처음에 free 제대로 안 해서 메모리 누수 났다

valgrind로 찾아냈다

valgrind --leak-check=full ./log_parser attack.log output.json

문제 코드

// 잘못된 코드
void free_log_collection(LogCollection *collection) {
    free(collection);  // entries 안 free
}

수정 코드

void free_log_collection(LogCollection *collection) {
    if (collection) {
        if (collection->entries) {
            free(collection->entries);  // 추가
        }
        free(collection);
    }
}

valgrind 없었으면 찾기 힘들었을 듯하다

2. strtok의 함정

strtok는 원본 문자열을 수정한다

이거 몰라서 한참 헤맸다

// 문제 상황
char *line = "2025-12-12 | SQL | SUCCESS";
char *token1 = strtok(line, "|");
char *token2 = strtok(line, "|");  // 이미 수정된 line

// 해결책
char temp[MAX_LINE_LENGTH];
strcpy(temp, line);
char *token = strtok(temp, "|");

원본 복사해서 사용해야 한다

3. JSON 특수문자 처리

처음엔 escape 안 했더니 JSON 파싱 실패하더라

// 잘못된 출력
{
  "payload": "' OR "1"="1"
}
// JSON 파서가 여기서 멈춤

// 올바른 출력
{
  "payload": "' OR \"1\"=\"1\""
}

특히 큰따옴표랑 백슬래시 조심해야 한다


Part 2: Logstash → OpenSearch 인증 구조

또 다른 고민: 안전한 인증

C 파서로 JSON 만들었으니 이제 OpenSearch에 넣어야 한다

여기서 새로운 고민이 생겼다

어떻게 안전하게 인증할 것인가?


초기 설계안: 비밀번호 방식

원래 계획

Terraform → 랜덤 비밀번호 생성
    ↓
OpenSearch 프로비저닝 시 비밀번호 설정
    ↓
Secrets Manager에 저장
    ↓
ECS가 Secret ARN 참조해서 Logstash 컨테이너에 전달

이렇게 하면 될 것 같았다

문제 1: Terraform State 문제

상황

resource "random_password" "opensearch_admin" {
  length  = 16
  special = true
}

resource "aws_opensearch_domain" "main" {
  # ...
  advanced_security_options {
    master_user_options {
      master_user_name     = "admin"
      master_user_password = random_password.opensearch_admin.result
    }
  }
}

이렇게 하면 Terraform state에 비밀번호가 평문으로 남는다

State 파일 확인해보니

{
  "resources": [
    {
      "type": "random_password",
      "instances": [
        {
          "attributes": {
            "result": "MyS3cr3tP@ssw0rd"  // 그냥 평문으로 박혀있음
          }
        }
      ]
    }
  ]
}

물론 우리 state는 S3에 있고 외부 접근 불가긴 하다

근데 팀원이면 다 볼 수 있다

이게 Best Practice는 아닌 것 같았다

문제 2: OpenSearch 프로비저닝 시점 문제

ECS는 편하다

resource "aws_ecs_task_definition" "logstash" {
  container_definitions = jsonencode([{
    secrets = [{
      name      = "OPENSEARCH_PASSWORD"
      valueFrom = aws_secretsmanager_secret.opensearch.arn
    }]
  }])
}

ECS가 알아서 Secret ARN 참조해서 컨테이너한테 환경변수로 넘겨준다

근데 OpenSearch는 다르다

OpenSearch는 프로비저닝 할 때 관리자 비밀번호를 미리 설정해야 한다

나중에 바꿀 수는 있는데, 처음 만들 때는 무조건 넣어야 한다

그래서 Terraform이 비밀번호를 알아야 하고

결국 state에 남는다

문제 3: 과한 권한

만약 Logstash 컨테이너가 털리면?

admin 비밀번호가 노출된다

OpenSearch 전체가 위험해진다

Logstash가 실제로 하는 일

  • 인덱스에 문서 쓰기
  • 필요하면 인덱스 자동 생성

이게 다다

클러스터 설정 바꾸거나 유저 관리 같은 관리자 권한은 전혀 필요 없다

그런데 admin 계정을 준다는 건 과한 권한이다


해결책: IAM 인증

왜 IAM 인증인가?

OpenSearch가 IAM(SigV4) 인증을 공식으로 지원한다

ECS/Fargate 환경에서는 Task Role 기반 인증이 권장 방식이다

장점 비교

항목비밀번호 방식IAM 방식
Terraform state평문 저장됨저장 안 됨
비밀번호 관리Secrets Manager 필요불필요
권한 세분화계정 단위IAM Policy 단위
자동 회전수동 관리STS 자동 회전
감사 추적제한적CloudTrail 통합
컨테이너 보안비밀번호 노출 위험Task Role로 안전

IAM 인증 작동 원리

SigV4 (AWS Signature Version 4)

AWS API 요청에 서명을 추가하는 방식이다

1. ECS Task가 Task Role 자격증명 획득
2. Logstash가 OpenSearch에 요청
3. 요청에 IAM 서명 추가 (SigV4)
4. OpenSearch가 IAM으로 검증
5. 인증 성공 시 요청 처리

핵심 개념: Task Role

ECS Task 자체가 신원이 된다

컨테이너 안에 비밀번호 같은 거 안 넣어도 된다

STS 임시 자격증명

Task Role은 임시 자격증명을 사용한다

Access Key ID: ASIAXXX...
Secret Access Key: wJalr...
Session Token: FwoGZXIv...
Expiration: 2025-12-15 12:00:00

보통 12시간마다 자동으로 만료되고 새로 발급된다

비밀번호처럼 평생 유효한 게 아니다


구현: Logstash 설정

기존 방식 (비밀번호)

output {
  elasticsearch {
    hosts => ["https://opensearch-endpoint"]
    index => "attack-logs-%{+yyyy.MM.dd}"
    
    user => "admin"
    password => "${OPENSEARCH_PASSWORD}"  # 환경변수에서 가져옴
    
    ssl_verification_mode => "none"
  }
}

이렇게 하면 컨테이너 환경변수에 비밀번호가 있어야 한다

IAM 방식

output {
  elasticsearch {
    hosts => ["https://opensearch-endpoint"]
    index => "${PROJECT_NAME}-siem-%{+yyyy.MM.dd}"
    
    auth_type => "aws_iam"  # IAM 인증 활성화
    region    => "${AWS_REGION}"
    
    ssl_verification_mode => "${SSL_VERIFY_MODE:none}"
  }
}

user/password 필드가 아예 없다

auth_type만 aws_iam으로 설정하면 끝이다

환경변수

# 비밀번호 방식 (필요한 것들)
OPENSEARCH_PASSWORD=MyS3cr3tP@ssw0rd

# IAM 방식 (필요한 것들)
AWS_REGION=ap-northeast-2
PROJECT_NAME=2sec-siem

비밀번호 자체가 필요 없다


Terraform 구성

IAM Policy Version이란?

Terraform이나 AWS CLI에서 IAM Policy 작성할 때 보이는 "Version": "2012-10-17" 이거 날짜가 뭔지 궁금했다

이건 Policy 생성 날짜가 아니다

AWS IAM Policy 문법의 버전이다

역사

  • 2008-10-17: IAM Policy 처음 나온 버전 (구버전)
  • 2012-10-17: 현재 표준 버전 (신버전)

2012년 10월 17일에 새로운 Policy 문법이 나왔고, 지금도 이게 최신 버전이다

그래서 모든 IAM Policy에 "Version": "2012-10-17" 쓴다

ECS Task Role 정의

# ECS Task에 할당할 IAM Role
resource "aws_iam_role" "logstash_task" {
  name = "logstash-task-role"
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"  # IAM Policy 문법 버전
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ecs-tasks.amazonaws.com"
      }
    }]
  })
}

# OpenSearch 쓰기 권한만 부여
resource "aws_iam_role_policy" "logstash_opensearch" {
  name = "opensearch-write-policy"
  role = aws_iam_role.logstash_task.id
  
  policy = jsonencode({
    Version = "2012-10-17"  # IAM Policy 문법 버전
    Statement = [{
      Effect = "Allow"
      Action = [
        "es:ESHttpPost",   # 문서 쓰기
        "es:ESHttpPut"     # 인덱스 생성
      ]
      Resource = "${aws_opensearch_domain.main.arn}/*"
    }]
  })
}

핵심 포인트

  1. ESHttpPost: 문서 쓰기 권한
  2. ESHttpPut: 인덱스 생성 권한
  3. 읽기 권한 (ESHttpGet) 없음
  4. 삭제 권한 (ESHttpDelete) 없음
  5. 클러스터 설정 권한 없음

최소 권한 원칙

Logstash가 필요한 것만 정확히 준다

OpenSearch 도메인 설정

resource "aws_opensearch_domain" "main" {
  domain_name = "2sec-siem"
  
  # IAM 인증 활성화
  advanced_security_options {
    enabled = true
    internal_user_database_enabled = false  # IAM만 사용
  }
  
  # 접근 정책
  access_policies = jsonencode({
    Version = "2012-10-17"  # IAM Policy 문법 버전
    Statement = [{
      Effect = "Allow"
      Principal = {
        AWS = aws_iam_role.logstash_task.arn
      }
      Action = "es:*"
      Resource = "${aws_opensearch_domain.main.arn}/*"
    }]
  })
}

주요 설정

  • internal_user_database_enabled = false: 내부 사용자 DB 비활성화, IAM만 사용
  • access_policies: Task Role만 접근 허용

ECS Task Definition

resource "aws_ecs_task_definition" "logstash" {
  family = "logstash"
  
  # Task Role 할당
  task_role_arn = aws_iam_role.logstash_task.arn
  
  container_definitions = jsonencode([{
    name  = "logstash"
    image = "docker.elastic.co/logstash/logstash:8.11.0"
    
    environment = [
      {
        name  = "AWS_REGION"
        value = "ap-northeast-2"
      },
      {
        name  = "PROJECT_NAME"
        value = "2sec-siem"
      }
    ]
    
    # 비밀번호 관련 환경변수 없음
  }])
}

secrets 섹션 자체가 필요 없다


권한 분리 전략

계정 역할 구분

계정/Role용도권한
admin사람이 대시보드 접근모든 권한
logstash_task_roleLogstash 컨테이너쓰기 전용
readonly_role분석가읽기 전용

admin 계정 사용 시나리오

  • 대시보드 접근
  • 인덱스 템플릿 설정
  • 보안 설정 변경
  • 비상 상황 대응

logstash_task_role 사용 시나리오

  • 로그 수집 (자동)
  • 인덱스 자동 생성
  • 문서 적재

admin 계정은 사람만 쓴다

서비스는 전부 Task Role 쓴다

보안 이점

시나리오: Logstash 컨테이너 탈취

공격자가 Logstash 컨테이너 접근 성공
    ↓
환경변수 확인
    ↓
비밀번호 없음 (IAM 방식이라)
    ↓
Task Role 자격증명만 있음
    ↓
쓰기 권한만 있어서 데이터 삭제/변조 불가
    ↓
최악의 경우: 쓰레기 로그만 넣을 수 있음

비밀번호 방식이었으면

공격자가 Logstash 컨테이너 접근 성공
    ↓
환경변수에서 admin 비밀번호 획득
    ↓
OpenSearch 전체 접근 가능
    ↓
모든 데이터 삭제/변조 가능
    ↓
클러스터 설정 변경 가능

차이가 명확하다


감사 추적 (CloudTrail)

IAM 방식의 장점

모든 OpenSearch 접근이 CloudTrail에 기록된다

{
  "eventTime": "2025-12-15T10:39:15Z",
  "eventSource": "es.amazonaws.com",
  "eventName": "ESHttpPost",
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "AROAXXXXXXXXX:logstash-task",
    "arn": "arn:aws:sts::123456789012:assumed-role/logstash-task-role/logstash-task"
  },
  "requestParameters": {
    "index": "2sec-siem-2025.12.15",
    "operation": "_doc"
  },
  "responseElements": {
    "result": "created",
    "_id": "abc123"
  },
  "sourceIPAddress": "10.0.1.25"
}

확인 가능한 정보

  • 언제: eventTime
  • 누가: userIdentity (Task Role)
  • 무엇을: eventName (ESHttpPost)
  • 어디에: index
  • 결과: responseElements

비밀번호 방식은 이런 추적이 제한적이다


실제 동작 확인

1. Task Role 자격증명 확인

컨테이너 안에서

# ECS Task Metadata 확인
curl ${ECS_CONTAINER_METADATA_URI_V4}/task

# IAM Role 자격증명 확인
curl 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI

출력 예시

{
  "AccessKeyId": "ASIAXXX...",
  "SecretAccessKey": "wJalr...",
  "Token": "FwoGZXIv...",
  "Expiration": "2025-12-15T22:39:15Z"
}

자동으로 발급된 임시 자격증명이다

2. Logstash 인증 로그

[2025-12-15T10:39:15,123][INFO ][logstash.outputs.elasticsearch] 
Using AWS IAM authentication
Region: ap-northeast-2
Service: es

[2025-12-15T10:39:15,456][INFO ][logstash.outputs.elasticsearch]
Successfully authenticated to OpenSearch
Index: 2sec-siem-2025.12.15

auth_type이 aws_iam이면 이런 로그 나온다

3. OpenSearch 접근 테스트

# Logstash 컨테이너 안에서
curl -X POST "https://opensearch-endpoint/2sec-siem-test/_doc" \
  --aws-sigv4 "aws:amz:ap-northeast-2:es" \
  -H "Content-Type: application/json" \
  -d '{"test": "data"}'

SigV4 서명 자동으로 추가된다


대안: ingest 전용 계정

만약 IAM 방식이 당장 부담된다면?

차선책이 있다

ingest 전용 계정 생성

OpenSearch 내부 사용자 DB에 ingest 전용 계정 만든다

# OpenSearch Dashboards에서
POST /_plugins/_security/api/internalusers/logstash_ingest
{
  "password": "랜덤생성비밀번호",
  "backend_roles": ["ingest_role"]
}

# Role 생성
POST /_plugins/_security/api/roles/ingest_role
{
  "cluster_permissions": ["cluster_composite_ops"],
  "index_permissions": [{
    "index_patterns": ["2sec-siem-*"],
    "allowed_actions": ["create_index", "write"]
  }]
}

Terraform으로 비밀번호 관리

# 비밀번호 생성
resource "random_password" "logstash_ingest" {
  length  = 32
  special = true
}

# Secrets Manager에 저장
resource "aws_secretsmanager_secret" "logstash" {
  name = "logstash-opensearch-password"
}

resource "aws_secretsmanager_secret_version" "logstash" {
  secret_id     = aws_secretsmanager_secret.logstash.id
  secret_string = random_password.logstash_ingest.result
}

# ECS Task Definition
resource "aws_ecs_task_definition" "logstash" {
  container_definitions = jsonencode([{
    secrets = [{
      name      = "OPENSEARCH_PASSWORD"
      valueFrom = aws_secretsmanager_secret.logstash.arn
    }]
  }])
}

장점

  • IAM 설정보다 간단하다
  • admin 계정보다는 안전하다
  • Terraform state 문제는 여전히 있다

단점

  • 비밀번호 관리 필요
  • 수동 회전 필요
  • CloudTrail 감사 제한적
  • state에 평문 남음

가능하면 IAM으로 가는 게 맞다


LLM CTI 연동

데이터 파이프라인

C Parser → JSON → Logstash → OpenSearch → LLM CTI

각 단계 설명

단계역할형식
C Parser로그 정제JSON
Logstash데이터 전송Bulk API
OpenSearch인덱싱Document
LLM CTI분석Embedding

LLM 학습 데이터 구성

입력 형식

{
  "timeline": [
    {
      "timestamp": "2025-12-12 10:39:15",
      "attack_type": "SQL_INJECTION",
      "payload": "' OR 1=1--",
      "success": false
    },
    {
      "timestamp": "2025-12-12 10:39:20",
      "attack_type": "SQL_INJECTION",
      "payload": "' UNION SELECT NULL--",
      "success": false
    },
    {
      "timestamp": "2025-12-12 10:39:25",
      "attack_type": "SQL_INJECTION",
      "payload": "' AND SLEEP(3)--",
      "success": true
    }
  ]
}

기대 출력

{
  "attack_stage": "reconnaissance → exploitation",
  "risk_level": "high",
  "recommendation": "Block source IP and patch SQL vulnerability"
}

LLM 학습 전략

1단계: RAG 기반 분석

과거 공격 패턴 벡터 DB에 저장

유사한 공격 발생 시 참조

2단계: 로그 누적

공격 성공/실패 케이스 수집

패턴 분석을 위한 데이터셋 확보

3단계: LoRA Fine-tuning

SIEM 로그 이해력 향상

특정 공격 유형 탐지 정확도 개선


마치며

C 파서 + IAM 인증 = 완벽한 조합

C 파서가 해결한 문제

  • 대용량 로그 빠른 처리
  • 구조화된 데이터 생성
  • LLM 학습 데이터 준비

IAM 인증이 해결한 문제

  • Terraform state 평문 저장
  • 과한 권한 부여
  • 비밀번호 관리 부담
  • 감사 추적 부족

두 가지를 합치니까 전체 파이프라인이 깔끔해졌다

배운 점

기술적 측면

  1. C 프로그래밍 실전 경험
  2. AWS IAM 인증 체계 이해
  3. ECS Task Role 활용법
  4. Terraform state 보안 이슈

설계적 측면

  1. 성능과 보안 둘 다 중요하다
  2. 최소 권한 원칙 실천
  3. 보안/운영 트레이드오프 균형

향후 계획

C 파서 개선

  • 멀티스레드 처리
  • 실시간 스트리밍
  • Protocol Buffer 지원

LLM CTI 연동

  • RAG 기반 공격 패턴 분석
  • Fine-tuning으로 탐지 정확도 향상
  • 자동 대응 전략 생성

모니터링 강화

  • CloudWatch 메트릭 수집
  • 이상 탐지 알람 설정
  • 대시보드 구축

기술 스택

분류기술
로그 파싱C (C99)
로그 수집Logstash 8.11
데이터 저장OpenSearch 2.11
인증AWS IAM (SigV4)
인프라ECS Fargate, Terraform
모니터링CloudWatch, CloudTrail
LLM 분석(구축 중)

참고 자료

profile
nyo님 좋아합니다!

0개의 댓글