본 콘텐츠는 사용자의 편의를 고려해 자동 기계 번역 서비스를 사용하였습니다. 영어 원문과 다른 오류, 누락 또는 해석상의 미묘한 차이가 포함될 수 있습니다. 필요하시다면 영어 원문을 참조하시기를 바랍니다.
보안 인사이트 는 모든 Cloudflare 계정에 대해 실행 가능한 보안 권장 사항을 제공합니다. 이러한 인사이트를 찾기 위해 모든 계정, 영역, DNS 레코드를 정기적으로 스캔하여 잠재적인 보안 위험과 잘못된 구성이 있는지 찾습니다.
하지만 두 가지 중요한 문제가 발생했습니다. 첫째, 스캔 빈도가 너무 낮았습니다. 검사는 한두 주에 한두 번만 수행되었으므로 새로 도입된 보안 위험은 최대 2주 동안 감지되지 않은 채로 남아 있을 수 있었습니다. 둘째, 많은 무료 요금제 계정이 자동 검사를 선택했는데, 이는 많은 계정이 전혀 검사되지 않는다는 것을 의미했습니다.
자동화된 공격이 가속화되면서 보안 구성 오류를 감지할 수 있는 윈도우는 줄어들고 있습니다. Cloudflare는 모든 고객을 위해 이러한 문제가 발생하도록 하는 것은 모두를 위해 더 나은 인터넷을 구축하려는 Cloudflare의 목표에 매우 중요합니다.
스캔 빈도를 높이고 모든 계정에 대한 자동 스캔을 활성화하려면 스캔 처리량을 평균 약 10배, 즉 초당 10회 스캔에서 초당 100회 스캔으로 늘려야 한다는 계산이 나왔습니다. 하지만 저희 시스템은 이미 로드로 인해 어려움을 겪고 있었습니다. 수백만 개의 이벤트가 처리를 기다리는 백로그를 가득 채우고 있었습니다. API가 자주 시간 초과되었습니다. 프로세스가 충돌했습니다. 시스템을 수리해야 했고 확장할 수 있어야 했습니다.
Cloudflare가 Security Insights의 스캔 처리량을 10배 이상 늘리고 수백만 고객에게 보안 인사이트를 제공하며 모든 고객을 대상으로 스캔 빈도를 두 배로 늘린 방법에 대한 이야기입니다. Cloudflare가 이러한 개선을 달성한 방법을 알아보려면 계속 읽어보세요.
Cloudflare가 보안 인사이트를 스캔하는 방법
높은 수준에서 자동 보안 검사는 스케줄러에 의해 트리거됩니다. 계정 또는 영역을 스캔해야 하는 경우 스케줄러는 오픈 소스 분산 이벤트 스트리밍 플랫폼인 Apache Kafka에 메시지(또는 메시지)를 게시합니다. 이러한 메시지는 특정 자산 또는 구성을 스캔하는 전문화된 Go 마이크로서비스인 여러 체커에 전달됩니다.
각 검사기는 모든 메시지에 대해 결과(발견한 보안 인사이트)를 내부 API로 전송하고, 내부 API는 이를 Postgres 데이터베이스에 저장합니다.
Apache Kafka는 엄밀히 말하면 큐가 아닙니다. 분할된 이벤트 스트림입니다(최근에 대기열 시맨틱을 갖게 되었지만). 파티션 내에서 메시지는 순서대로 소비되고 처리 되어야 합니다. 이는 메시지가 순서대로 사용될 수 있지만 순서대로 처리되지 않는 일반적인 대기열과 다릅니다. 따라서 소비자 그룹 내에서는 파티션당 하나의 소비자만 활성화될 수 있습니다.
이로 인해 다음과 같은 두 가지 결과가 초래됩니다.
파티션을 더 추가하여 확장을 시도할 수도 있었습니다. 그러나 이렇게 하면 다른 많은 서비스에서 공유하는 Kafka 브로커 자체의 리소스 사용량이 증가합니다. 우리는 코드와 아키텍처를 우선적으로 개선하기 위해 마지막 수단으로 이 옵션을 예약했습니다.
메시지를 순서대로 소비할 수는 있지만, 한 번에 여러 메시지를 사용하는 것을 막을 수는 없습니다.
우리는 검사기가 일괄적으로 메시지를 소비하여 별도의 고루틴에서 각 메시지를 처리하도록 변경했습니다. 단점은 배치 중간에 프로세스가 중단되면 다시 수행해야 하는 작업이 더 많고 메모리 사용량이 약간 증가한다는 것입니다. 우리의 경우, 이 두 가지는 모두 수용 가능했습니다.
일부 검사기에서 처리되는 일부 메시지는 다른 메시지보다 처리 시간이 훨씬 깁니다. 예를 들어, 한 계정/영역에는 다른 계정/영역보다 자산이 훨씬 더 많을 수 있습니다. 최악의 경우 이러한 메시지를 처리하는 데 평균 몇 초 또는 밀리초가 소요될 수 있습니다.
우리는 소비자 그룹과 체커를 '저속 차선'과 '빠른 차선'으로 분리하는 아주 간단한 접근 방식을 선택했습니다. Cloudflare에서는 메시지를 느리게 처리할지 빠르게 처리할지 빠르게 결정할 수 있었습니다. '고속 차선' 검사기는 느린 메시지를 발견하면 건너뛰게 됩니다.
이를 통해 문제가 해결되었습니다. 느린 메시지는 최소한의 지연으로 처리할 수 있는 전용 리소스와 시간이 확보되었고, 빠른 메시지는 평소의 빠른 속도로 진행될 수 있었습니다.
Cloudflare가 찾은 모든 인사이트는 Postgres 데이터베이스에 기록됩니다. 이는 Cloudflare의 체커가 인사이트 목록과 함께 호출하는 단일 API 엔드포인트에서 처리합니다. 구현 과정은 다음과 같습니다.
for _, issue := range issues {
_, err = tx.Exec(ctx, `INSERT INTO table ... VALUES ($1, $2, ...) ON CONFLICT DO UPDATE ...`, ...)
if err != nil {
return err
}
}
예리한 독자는 인사이트가 많은 경우, 이 코드가 인사이트마다 데이터베이스를 왕복한다는 것을 알 수 있습니다. 최대 500,000개의 관찰된 크기를 보면, 단일 API 호출로 인한 왕복, 쿼리, 트랜잭션은 50만 건에 달했습니다.
우리는 처음에 Postgres: COPY 임시 테이블에 대량 삽입을 위한 최적의 표준을 시도했습니다. 그러나 우리는 이러한 접근 방식으로 인해 Postgres 시스템 테이블이 비대해지는 것을 발견했습니다.
Cloudflare는 하이브리드 접근 방식으로 결정했습니다.
이렇게 하면 엄청난 인사이트 세트를 위한 합리적으로 빠른 삽입(초)과 작은 인사이트 세트를 위한 더 빠른 삽입(밀리초)의 두 가지 장점이 제공되었습니다.
규모를 조정하려고 시도하는 동안 내부 API에서 몇 가지 이상한 동작을 발견했습니다.
많은 요청으로 인해 클라이언트 측 제한 시간 초과가 트리거되었습니다
많은 검사기가 처리 시간의 20~90%를 단일 API 호출에 소비하고 있었습니다
많은 양의 스캔을 트리거하면 처리량이 높게 시작하여 저하됩니다
이 모든 문제의 근본 원인은 대기 시간이었습니다.
Cloudflare의 기본 데이터베이스는 오리건주 포틀랜드에 있습니다. 하지만 우리 API는 포틀랜드와 암스테르담 모두에서 액티브-액티브로 실행되고 있었습니다. 빛의 속도라고 해도 포틀랜드와 암스테르담 간의 왕복 대기 시간은 50밀리초입니다.
이 대기 시간으로 인해 암스테르담 API 인스턴스의 데이터베이스 쿼리는 훨씬 더 오래 걸렸고, 클라이언트 측 연결 풀의 연결은 열린 상태로 유지되었습니다. API에 대한 대량의 요청으로 연결 풀이 빠르게 소진되어 무료 연결을 기다리는 동안 제한 시간 초과가 발생했습니다. Cloudflare의 API 호출은 포틀랜드에서는 평균 10ms 이내에 완료되지만, 암스테르담에서는 거의 3초 내에 완료됩니다!
그런데 왜 메시지 처리량이 감소할까요? 각 검사기 프로세스에는 사용할 Kafka 스트림의 파티션 세트가 할당됩니다. Cloudflare의 API는 부하가 분산되었습니다. 우리는 프로세스의 수명 주기 내내 연결을 유지해 놓았으므로 일부 프로세스는 암스테르담 API에 연결되어 있고 다른 프로세스는 포틀랜드 API에 연결되어 있습니다. 포틀랜드와 링크된 파티션은 빠르게 처리되었지만, 암스테르담으로 향하는 프로세스에서 사용된 파티션은 그보다 뒤처져 있었습니다.
Cloudflare 검사기 중 하나의 파티션별 카프카 지연(단일 소비자 그룹 내에서 처리를 기다리는 메시지 수)입니다. 이 경우에는 30개의 파티션이 있습니다. 정확히 15개의 파티션이 뒤처져 있는 것을 볼 수 있습니다(03/10 03:00경보다 0에 도달하거나 가까워지는 선). 이는 부하 분산 장치가 API 엔드포인트 간에 트래픽을 균등하게 분할하기 때문입니다.
이것은 간단한 수정이었습니다. 우리는 API를 활성-수동으로 전환하여 활성 API가 기본 데이터베이스를 따르도록 했습니다. 대기 시간 문제는 하룻밤 사이에 사라졌습니다.
우리는 Kafka를 확장했습니다. 데이터베이스 쿼리를 최적화했습니다. API를 수정했습니다. 하지만 여전히 문제가 발생했습니다. 스캔 시간을 대략적으로 균일하게 분산시켜야 했습니다. Kafka 주제가 시간 기반 보존 정책을 사용하기 때문에 모든 스캔을 동시에 대기열에 넣는 것은 가능하지 않았습니다.
스케줄러는 스캔을 균일하게 분산하지 못했습니다. 주어진 시간에 트리거되는 스캔 수는 급증했고 예측할 수 없었습니다. 일주일 내내 특정 시점에서 수십만 건의 검사가 몇 분 이내에 서로 트리거되기도 합니다. 무슨 일이죠?
스케줄러는 고정된 반복 기간에 스캔을 트리거합니다. 의사 코드에서 스케줄러는 다음과 같은 모습입니다.
Loop forever:
Find accounts where last_scheduled_at + scanning frequency <= now
For each account:
Trigger scan for account
Trigger scan for all zones in the account
Update last_scheduled_at = now
Cloudflare는 데이터베이스의 다수의 계정에서 last_scheduled_at이 유사하다는 것을 빠르게 발견했으며, 이는 이러한 불균등의 원인이 되었습니다.
그러나 완벽하게 고른 분포라 하더라도 스캔 빈도를 늘리면 이 문제가 더욱 악화될 수 있습니다. 예를 들어, 스캔 빈도를 15일마다에서 7일마다로 변경하면 계정의 53%가 갑자기 스캔해야 합니다.
이 논리에는 또 다른 문제가 있었습니다. 일부 계정에는 매우 많은 수의 영역이 있습니다. 이러한 계정을 예약한 시점에는 모든 영역에 대한 스캔이 연쇄적으로 이루어졌습니다. 이로 인해 Kafka 파티션이 포화되어 훨씬 더 작은 계정을 검사하는 데 지연이 발생했습니다.
이 문제를 해결하기 위해 세 가지 주요 변경 사항을 적용했습니다.
계정과 관계없이 구간을 예약합니다. 각 구간에는 고유한 last_scheduled_at 필드가 있습니다.
기존 계정과 구간의 last_scheduled_at 시간을 무작위로 설정합니다.
스캔 예약을 위한 적응형 속도 제한을 도입합니다.
영역을 독립적으로 예약하는 것은 대규모 계정의 문제를 해결하는 확실한 방법이었습니다. last_scheduled_at 시간을 무작위로 지정(그리고 이 프로세스 중에 스캔이 지연되지 않도록)하여 데이터베이스에 기존의 불균등성을 수정할 수 있었습니다.
적응형 레이트 리미팅은 약간 더 흥미롭습니다. 레이트 리미팅을 사용하면 스캔 주파수를 변경할 때 스캔 급증 문제를 해결할 수 있습니다. 예를 들어, 스캔 빈도를 7일마다로 늘리고자 하는데 계정이 5천만 개일 때, 레이트 리미팅을 83회/초로 설정하면 7일 동안 고르게 스캔할 수 있습니다.
하지만 계정이 천만 개 더 추가된다면 어떨까요? 이 경우 이 속도 제한으로 인해 모든 계정을 스캔하는 데 8일 이 소요됩니다. 여기에서 적응형 부분이 등장합니다. 전체 계정 및 영역 수와 스캔 빈도에 따라 30분마다 레이트 리미팅을 비동기식으로 재계산하는 것입니다. 이렇게 하면 수천 또는 수백만 개의 계정과 영역을 더 온보딩하는 경우에도 스캔을 정시에 계속할 수 있습니다.
func computeRate(free, pro, biz, ent int64) rate.Limit {
r := float64(free)/freeScanInterval.Seconds() +
float64(pro)/proScanInterval.Seconds() +
float64(biz)/bizScanInterval.Seconds() +
float64(ent)/entScanInterval.Seconds()
// Guard against zero counts. We always want to schedule at least one scan per second.
if r < 1 {
r = 1
}
// Increase rate limit beyond the 'perfect' value, to have a buffer in case of any downtime
// or spikes in load.
r *= rateLimitBufferFactor
return rate.Limit(r)
}
이러한 수정으로 시간 경과에 따른 검사기당 7일 이동 평균 처리량은 10배 이상 증가했습니다.
이러한 개선 이전에는 초당 약 10개의 스캔을 실행했습니다. 이 속도와 초당 100회의 스캔 처리량이라는 우리 목표의 차이는 큰 것 같았습니다. 우리는 Kafka 주제에 더 많은 리소스를 투입하고, Kafka 주제에 대해 더 많은 파티션을 추가하는 것에 대해 논의했으며, 심지어 전체 아키텍처를 폐기할 수도 있었습니다.
하지만 Cloudflare의 수정 사항으로 모든 것이 달라졌습니다. 현재, Security Insights는 피크 예약 시간에도 초당 120개 이상의 검사를 수행하며, 개선 목표의 10배를 초과하고 있습니다. 내부 API 시간이 더 이상 초과되지 않으며 Kafka 지연 메트릭이 훨씬 더 건강해 보입니다. 이러한 확장성 개선을 통해 모든 무료 계정 및 영역에 대해 자동 스캔을 설정하고 모든 고객에 대해 스캔 빈도를 늘릴 수 있었습니다.
무료: 7일마다
프로 및 비즈니스 요금제: 3일마다
Enterprise: 매일
시스템 안정성이 향상되어 이전에는 생성하기 어려웠던 새로운 기능을 구축할 수 있다는 자신감이 생겼습니다. 세분화된 주문형 검색을 수행하는 기능을 추가했습니다. 이제 Cloudflare 계정, 영역, 인사이트 또는 인사이트 유형을 수동으로 다시 스캔할 수 있습니다.
Cloudflare 대시보드의 보안 개요 페이지 에서 세분화된 주문형 스캔 시작
우리는 아무것도 버리기 전에 기존 시스템을 깊이 이해하는 것이 중요하다는 교훈을 얻었습니다. 코드, SQL 쿼리, 로그, 메트릭(특히 메트릭!)을 면밀히 살펴보면서 단순히 포드나 파티션을 추가하지 않고도 용량을 늘릴 수 있었습니다. 우리의 가정에 의문을 제기하고, 이상하게 보이는 메트릭을 파헤친 다음, 쉬운 방법(예: API 클라이언트 측 제한 시간 초과 증가)을 거부함으로써 우리는 더 안정적이고 탄력적인 시스템을 구축했습니다.
문제에 더 많은 리소스를 투입하는 것이 때로는 해결책이 될 수도 있지만, Cloudflare에서는 문제를 해결하기 위해 엔지니어링하는 것이 중요하다고 믿습니다.
Security Insights 스캔은 모든 Cloudflare 요금제에서 기본적으로 활성화되어 있습니다. 지금 바로 Cloudflare 대시보드 에 로그인하여 보안 인사이트를 검토하고 관리하세요.