Documentation

/

White Papers

개발 속도와 안정성 향상이 검증된 QueryPie의 DevSecOps 파이프라인

jake, ravi, noah

2024년 11월 22일

개발 속도와 안정성 향상이 검증된 QueryPie의 DevSecOps 파이프라인

개요

보안을 통합한 QueryPie의 파이프라인

QueryPie가 중요하게 생각하는 부분, 빠른 개발 속도와 안정성입니다. QueryPie 팀은 빠른 성장을 원합니다. 그리고 변화에 민첩하게 대응하길 원합니다. 그러나 간과하면 안되는 부분이 있습니다. 우리는 개발 속도를 높이다 보면 보안 검토가 뒤로 밀리거나 간과되는 경우를 초기에 많이 경험하였습니다. 이로 인하여 출시 후 보안 취약점이 발견되면 긴급 패치, 높은 비용, 그리고 브랜드 신뢰도 손실로 이어질 수 있다는 위험을 충분히 인지하고 있습니다.

DevSecOps는 이러한 문제를 근본적으로 해결하기 위한 접근 방식입니다. 개발(Dev)과 운영(Ops) 과정에 보안(Security)을 통합하여, 개발 초기부터 운영 단계까지 보안을 자동화하고 표준화된 방식으로 QueryPie는 파이프라인을 구축 운영하고 있습니다.

QueryPie의 개발 라이프사이클 전 단계 보안성 검토 프로세스

QueryPie는 Privileged Access Management(PAM) 솔루션으로서, Database Access Control(DAC), System Access Control(SAC), Kubernetes Access Control(KAC) 등 중요한 자산과 정보를 보호하기 위해 고도의 보안을 요구하는 제품입니다. 이에 따라 더욱더 철저한 보안 관리를 위하여 QueryPie는 개발 라이프사이클 전반에 걸쳐 일관된 보안 검토 및 점검이 이루어집니다. 개발 초기 단계의 요구사항 수립부터, 설계, 구현, 테스트, 배포, 운영에 이르기까지 각 단계에서 다층적인 보안 검토 프로세스를 적용하여 잠재적인 보안 위협을 사전에 방지하고 있습니다. 이를 구현하기 위해서 저희는 DevSecOps 접근 방식을 채택하여 개발과 보안을 통합하여 긴밀히 연계하고 자동화된 보안 검사와 지속적인 모니터링으로 취약점을 신속하게 식별하고 대응함으로써, 제품의 안정성과 신뢰성을 더욱 강화하고 있습니다. 이처럼, 우리에게 보안은 단순한 기능을 넘어 제품의 핵심 요소로 자리 잡고 있습니다.

QueryPie CI/CD Pipeline
QueryPie CI/CD 파이프라인

DevSecOps의 필요성과 클라우드 환경에서의 보안 과제

기존의 보안 접근법은 보안에 대한 검토를 개발 완료 후에 수행하는 경우가 많아, 문제 발생 시 수정에 시간이 오래 걸리고 비용이 높아지는 경우가 대부분 이었습니다. 예를 들어, 코드가 완성된 후 발견된 보안 취약점을 수정하려면 이미 작성된 코드나 시스템 아키텍처를 다시 분석하고 변경해야 합니다. 이는 초기 단계에서 문제가 발견될 때보다 훨씬 더 복잡하고 더 많이 비용의 발생을 초래 합니다.

그러나 DevSecOps는 보안을 개발 초기에 통합함으로써 이러한 문제를 해결할 수 있습니다. 자동화된 보안 검토와 주기적이고 지속적인 보안 점검을 통해 QueryPie는 개발 효율성을 유지하면서도 안전한 클라우드 환경을 제공할 수 있습니다. 또한 클라우드 환경에서는 다수의 인프라 구성과 관리가 자동화되므로, 보안 취약점이 빠르게 노출될 위험이 있습니다. 따라서 QueryPie의 DevSecOps 파이프라인은 클라우드 환경이 가지고 있는 여러 보안 과제를 해결하고, 안전한 제품 운영을 지속하기 위해 끊임없이 개선하고 있습니다.

QueryPie의 DevSecOps 파이프라인 구축을 통한 신뢰성 확보

QueryPie는 DevSecOps 파이프라인을 통해 보안을 자동화하고 클라우드 환경의 보안 과제를 적극적으로 해결하며, 신뢰할 수 있는 PAM 솔루션으로 자리매김하고자 합니다. 본 백서에서는 QueryPie의 개발 라이프사이클 전 단계에서 이루어지는 보안성 검토와 DevSecOps 파이프라인의 구축 과정을 통해 보안이 어떻게 제품 신뢰성에 기여하는지를 자세히 설명하고자 합니다.

CI/CD 파이프라인 구축과 자동화

QueryPie에서는 Github Action을 통해 CI/CD 파이프라인이 구축되어 있으며, 아래 각 단계별로 보안 체크포인트를 배치하여 자동으로 보안 검사를 수행하고 있습니다.
단계별 취약점 진단을 통해 Medium 이상의 취약점 발견된 경우, 취약점 조치가 된 이후에 다음 단계로 진행될 수 있도록 제어하고 있습니다. 관리되는 취약점 유형은 아래 이미지와 같이 CI/CD 파이프라인 전 단계에서 다양한 유형의 취약점을 점검 및 식별하고 제거하는 관리 프로세스를 구축하고 있습니다.

Types of vulnerabilities to be checked in the CI/CD pipeline

QueryPie DecSecOps 단계

STEP 0) 배포를 위한 이미지를 취약점 없는 깨끗한 골든 이미지로 관리합니다.
STEP 1) SCA, SAST 점검을 통해 소스코드 취약점 및 오픈소스 Dependency를 점검합니다.
STEP 2) 배포되는 컨테이너 이미지에 대한 취약점을 스캔합니다.
STEP 3) DAST와 모의해킹을 병행하여 어플리케이션 취약점을 점검합니다.

Vulnerability Scanning Tools in the CI/CD Pipeline
CI/CD 파이프라인의 취약점 점검 도구
Report of Vulnerability Scanning in the CI/CD Pipeline
CI/CD 파이프라인의 취약점 점검 현황

Golden Image 관리

QueryPie에서는 고객 배포용 이미지내부 테스트용 이미지를 분리해서 사용하고 있습니다.

각 이미지는 CIS Benchmark Level 1 및 CVE 취약점을 제거한 Golden Image로 생성하여 관리중이며, 이외 별도의 이미지는 사용이 불가하도록 통제하고 있습니다.

이미지 하드닝은 보안팀에서 자체 제작한 스크립트를 통해 CIS Benchmark Level 1 및 CVE 취약점을 제거합니다.

다음은 OS Image Hardening 을 위한 CIS Benchmark 점검 항목 입니다.

카테고리

세부 점검 항목

1. Initial Setup

1.1 Filesystem

1.2 Configure Software and Patch Management

1.3 Configure Secure Boot Settings

1.4 Configure Additional Process Hardening

1.5 Mandatory Access Control

1.6 Configure Command Line Warning Banners

2. Services

2.1 Configure Time Synchronization

2.2 Configure Special Purpose Services

2.3 Configure Service Clients

3. Network Configuration

3.1 Configure Network Devices

3.2 Configure Network Kernel Modules

3.3 Configure Network Kernel Parameters

3.4 Configure Host Based Firewall

4. Access, Authentication,
and Authorization

4.1 Configure Job Schedulers

4.2 Configure SSH Server

4.3 Configure Privilege Escalation

4.4 Configure Pluggable Authentication Modules

4.5 User Accounts and Environment

5. Logging and Auditing

5.1 Configure Logging

5.2 Configure System Accounting (auditd)

5.3 Configure Integrity Checking

6. System Maintenance

6.1 System File Permissions

6.2 Local User and Group Settings

위 점검 항목을 기반으로 아래와 같이 Remediation Script 를 작성하고 자동으로 Configuration 을 조정 합니다.

CIS Benchmark Level 1 - Hardening Process
CIS Benchmark Level 1 - Hardening Process

코드 보안 검사 및 종속성 관리

개발 소스코드의 보안 취약점을 식별하기 위해 사용하는 SAST 도구는 벤더사가 제공하는 기본 탐지 룰만으로는 충분하지 않은 경우가 종종 있습니다. 아래는 취약점이 포함된 공개 샘플 코드를 대상으로 기본 탐지 룰을 활용한 취약점 탐지 결과를 보여줍니다.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
package ai.qwiet.springbootkotlinwebgoat import org.springframework.http.HttpHeadersimport org.springframework.web.bind.annotation.GetMappingimport org.springframework.web.bind.annotation.RequestParamimport org.springframework.web.bind.annotation.RestControllerimport org.springframework.http.HttpStatusimport org.springframework.http.ResponseEntityimport java.io.InputStreamReaderimport java.io.BufferedReaderimport java.io.Fileimport mu.KotlinLoggingimport org.apache.logging.log4j.LogManager @RestControllerclass HelloController {    val logger = KotlinLogging.logger {}    val secondaryLogger = LogManager.getLogger()     @GetMapping("/")    fun index(): String {        return "Greetings from Spring Boot!"    }     @GetMapping("/greet")    fun greet(@RequestParam("username") username: String): String {        logger.info { "Got request for `/greet`" }        // vulnerability: Sensitive Data Leak        secondaryLogger.debug("Params for `/greet `: $username")        // vulnerability: XSS        return "Greetings ${username}!"    }     fun parseParams(name: String, msg: String): Map {        val checkedName = name.takeUnless { it contains '\\' }?.ifBlank { "default_name" }        val checkedMsg = msg.ifBlank { "default_msg" }        return mapOf("parsed_name" to checkedName, "parsed_msg" to checkedMsg)    }     @GetMapping("/exec")    fun exec(@RequestParam("cmd") cmd: String): ResponseEntity {        logger.info { "Got request for `/exec`!" }        secondaryLogger.debug("Params for `/exec`: $cmd")         var out = "NOP"        if (cmd != "nop") {            // vulnerability: Remote Code Execution            val proc = Runtime.getRuntime().exec(cmd)            val lineReader = BufferedReader(InputStreamReader(proc.getInputStream()))            val output = StringBuilder()            lineReader.lines().forEach { line ->                output.append(line + "\n")            }            out = "Did execute command `$cmd`, got stdout: $output"        }        return ResponseEntity(out, HttpStatus.OK)    }     @GetMapping("/touch_file")    fun touchFile(@RequestParam("name") name: String, @RequestParam("msg") msg: String): ResponseEntity {        logger.info { "Got request for `/touch_file`!" }        secondaryLogger.debug("Params for `/touch_file`: $name | $msg")        if (name.length < 3) {            logger.warn { "The provided name is very short!" }        }         if (name == null || msg == null) {            return ResponseEntity("The `name` & `msg` parameters have to be set.", HttpStatus.OK)        } else {            val parsedParams = parseParams(name, msg)            val fullPath = "/tmp/http4kexample/" + parsedParams["parsed_name"]            val finalMsg = "MESSAGE: " + parsedParams["parsed_msg"]            // vulnerability: Directory Traversal            File(fullPath).writeText(finalMsg)            return ResponseEntity("Did write message `$finalMsg` to file at `$fullPath`", HttpStatus.OK)        }    }     @GetMapping("/debug")    fun debug(@RequestParam("url") url: String): ResponseEntity {        logger.info { "Got request for `/debug`!" }        secondaryLogger.debug("Params for `/debug`: $url")         val headers = HttpHeaders()        headers.add("Location", url)        // vulnerability: Open Redirect        return ResponseEntity(headers, HttpStatus.FOUND)    }     @GetMapping("/render_html")    fun renderHtml(@RequestParam("name") name: String): ResponseEntity {        logger.info { "Got request for `/render_html`!" }        secondaryLogger.debug("Params for `/render_html`: $name")         // vulnerability: XSS        val out = StringBuilder().append("<h1>Hello there, ").append("$name").append("!</h1>").toString()        return ResponseEntity(out, HttpStatus.OK)    }     @GetMapping("/add")    fun add(@RequestParam("x") x: String, @RequestParam("y") y: String): ResponseEntity {        logger.info { "Got request for `/add`!" }        secondaryLogger.debug("Params for `/add`: $x | $y")         val xi = x.toInt()        val xy = y.toInt()        val out = (xi + xy).toString()        return ResponseEntity(out, HttpStatus.OK)    }}