การทำ DevSecOps ด้วย OWASP ZAP

4 ธันวาคม 2023

พิชญะ โมริโมโต
พิชญะ โมริโมโตหัวหน้าทีมทดสอบเจาะแฮกระบบ (lead penetration tester) ของบริษัท สยามถนัดแฮก, เป็นที่ปรึกษาด้านความปลอดภัยให้หน่วยงานเอกชน, เป็นที่รู้จักกันในฐานะ หนึ่งในแอดมินกลุ่ม 2600 Thailand และเป็นหนึ่งในคนเขียนบทความลงเพจ สอนแฮกเว็บแบบแมว ๆ

ในบทความนี้เราจะมาดู วิธีการทำ DevSecOps ด้วย OWASP ZAP กัน โดยก่อนอื่นขอแนะนำพื้นฐานความเข้าใจคร่าว ๆ ก่อนว่า แต่ละส่วนคืออะไรประกอบด้วยอะไรและทำหน้าที่อะไร เพื่อที่จะสามารถนำไปสู่วิธีการทำทีละขั้นตอน ให้ลองทำตามกันได้

DevSecOps คืออะไร

ก่อนอื่นขออธิบาย DevSecOps คร่าว ๆ เสียก่อน ซึ่ง DevSecOps นั้นเป็นการรวมคำระหว่าง DevOps กับ Sec(urity)  

คำว่า DevOps ก็คือกระบวนการกับระบบที่ช่วยให้ทีม Development กับทีม Operations มาทำงานร่วมกันอย่างแน่นแฟ้นขึ้น เพื่อทำให้เกิดกระบวนการอัตโนมัติในการทำงานระหว่างทีม (Automation) ก็คือสิ่งไหนที่เป็นงานซ้ำ ๆ ไม่ว่าจะเป็นพวกงาน คอยตรวจสอบความถูกต้อง (Test) เอาแอปฯ ขึ้นไปติดตั้งบนเซิร์ฟเวอร์ (Deploy) รวมถึงสอดส่องสถานะระบบ ต่าง ๆ (Monitor) ก็จะพยายามทำให้มันเป็น สิ่งอัตโนมัติทั้งหมดเท่าที่ทำได้ เพื่อให้ลดงานที่คนต้องเข้ามาเกี่ยวข้อง ลดความผิดพลาด ให้ทำงานง่ายขึ้น เร็วขึ้น สะดวกขึ้น 

ส่วนคำว่า Sec หรือ Security ที่เพิ่มเข้ามาอยู่ตรงกลาง เพื่อใส่เรื่องความปลอดภัย เข้าไปในกระบวนการทำ Automation โดย DevSecOps ก็คือการเพิ่มกระบวนการกับระบบของทีม Security เข้ามาแจมกับทีม Development และทีม Operations ก่อนหน้าใน DevOps นั่นเอง

ขั้นตอนของการทำ DevSecOps ฉบับเข้าใจง่าย ก็จะเป็นดังภาพที่ 1 หมายความว่า Sec ไปคั่นอยู่ตรงกลาง ก่อนที่ Dev จะไป Ops นั่นเอง

ที่มา: https://www.hackerone.com/application-security/5-security-stages-devsecops-pipeline

การเอา Sec มาคั่นระหว่าง Dev กับ Ops 

ส่วนมากแล้วจะเป็นการใส่โปรแกรมที่เพิ่มขั้นตอนทางด้านความปลอดภัยต่าง ๆ เข้าไปตัวอย่างเช่น

  • การทำ Static Application Security Testing (SAST) อัตโนมัติ
    • การสแกนค้นหาช่องโหว่ใน Software Library ที่ใช้งาน
      • ตัวอย่างโปรแกรม OWASP Dependency Check และ snyk
    • การสแกนค้นหาช่องโหว่ในโค้ด (Security Source Code Scan)
      • ตัวอย่างโปรแกรม SonarQube และ Semgrep
    • การสแกนหาการฝังรหัสผ่านไว้ในโค้ดอย่างไม่ปลอดภัย
      • ตัวอย่างโปรแกรมเช่น gitleaks, git-secret
  • การทำ Dynamic Application Security Testing (DAST) อัตโนมัติ
    • การสแกนค้นหาช่องโหว่ด้วยโปรแกรม Vulnerability Assessment (VA)
      • ตัวอย่างโปรแกรมเช่น OpenVAS, nuclei, OWASP ZAP, Dastardly 
  • การสแกนค้นหาช่องโหว่ใน Layer อื่น ๆ เช่น Container ที่สร้างมาหรือโค้ดที่ใช้ทำ Infrastructure as a Code (IaC)
    • ตัวอย่างโปรแกรมเช่น clair, trivy, checkov, terrascan, tfsec
  • การบริหารจัดการช่องโหว่ที่เจอจาก SAST หรือ DAST ในขั้นตอนก่อนหน้า (ส่งมากองรวมกันที่เดียวจะได้จัดการในภาพรวมได้ง่ายขึ้น)
    • ตัวอย่างโปรแกรมเช่น ArcherySec และ Defect Dojo

ซึ่งการทำ DAST ด้วยโปรแกรม OWASP ZAP เราเคยพูดถึงกันไปแล้วในบทความ “ลองทำ Vulnerability Assessment (VA) ในองค์กรด้วยตัวเองแบบง่าย ๆ” (https://www.cyfence.com/article/how-to-va-pentest-in-your-organization/) สามารถตามไปย้อนอ่านกันได้

จากเดิมที่เราใช้คนเพื่อกดสแกนค้นหาช่องโหว่ ในบทความนี้เราจะมาต่อยอดกันด้วยการสแกนอย่างอัตโนมัติ ใน CI/CD Pipeline ที่จังหวะหลังจากที่ระบบอัตโนมัติ เอาแอปพลิเคชัน ไปติดตั้งบนเซิร์ฟเวอร์ (Deploy) แล้ว ก็จะทำ DAST เพื่อสแกนค้นหาช่องโหว่เว็บแอปพลิเคชันทันที

ส่วนประกอบของการทดสอบการทำ DevSecOps โดยในบทความความนี้จะใช้เครื่องมือดังต่อไปนี้ OWASP ZAP, Jenkins, GitLab, Nexus และ OWASP Juice Shop มาดูรายรายละเอียดในแต่ละเครื่องมือกัน


1. OWASP ZAP

OWASP ZAP ย่อมาจาก “Open Web Application Security Project Zed Attack Proxy” ซึ่งเป็นโปรแกรมไม่เสียค่าใช้จ่ายและยังเปิดเผยโค้ดเป็นสาธารณะ ที่ใช้สำหรับการทดสอบความปลอดภัยของเว็บแอปพลิเคชัน (Web Application Security Scanner)

โดยการใช้งานปกติของโปรแกรม OWASP ZAP จะเปิดโปรแกรมออกมาแล้วมีหน้าต่างโปรแกรม (GUI) และเมนูต่าง ๆ ให้เลือกใช้งาน ดังภาพที่ 2

ภาพที่ 2

แต่ สำหรับบทความนี้จะใช้ใน รูปแบบ Headless โดยจะเป็นรูปแบบที่ไม่มี GUI ซึ่งช่วยให้สามารถใช้ ร่วมกับสคริปต์อัตโนมัติของ CI/CD Pipeline เพื่อคำสั่งควบคุมการสแกนค้นหาช่องโหว่ ได้โดยไม่ต้องใช้คนเข้ามากดปุ่มในเมนูต่าง ๆ นั่นเอง

ฟังก์ชันหลัก ๆ ใน OWASP ZAP ที่จะถูกใช้ใน DevSecOps ก็คือการทำ Active Scan ที่จะเป็นการระบุ URL ของเว็บเป้าหมายที่เรา Deploy เสร็จสิ้นเข้าไป จากนั้น OWASP ZAP จะทำการสำรวจหาหน้าเว็บต่าง ๆ (Spider) และทดสอบยิงคำสั่งอันตรายรูปแบบต่าง ๆ (Fuzz) เพื่อตรวจสอบว่าจุดที่รับข้อมูลต่าง ๆ บนหน้าเว็บเหล่านั้น มีความเสี่ยงจะเกิดช่องโหว่อะไรหรือเปล่า นอกเหนือจากนั้นแล้วก็จะมีช่องโหว่ที่อาจหาเจอได้จากฟังก์ชัน Passive Scan เช่นการขาดการตั้งค่าทางด้านความปลอดภัยในจุดต่าง ๆ ของเว็บอย่าง HTTP Response Header และ Cookie เป็นต้น

1.1 OWASP ZAP Docker (zap-full-scan.py)

โดยวิธีการใช้งานรูปแบบ Headless ทำได้หลากหลายวิธี แต่ที่ได้รับความนิยมคือใช้ผ่านสคริปต์โค้ด Python ของ OWASP ZAP ในรูปแบบ Docker (https://github.com/zaproxy/zaproxy/tree/main/docker) โดยสคริปต์หลัก ๆ จะมี 3 ไฟล์ที่สำคัญสำหรับการเรียกใช้งานโดยตรงได้แก่

  1. zap-baseline.py เป็นสคริปต์ที่เน้นสแกนค้นหาช่องโหว่เว็บเฉพาะรูปแบบ Passive Scan ที่จะไม่ทำการ Fuzz ข้อมูลอะไรเข้าไปในเว็บ ซึ่งมักจะไม่สามารถหาช่องโหว่ความเสี่ยงสูงต่าง ๆ ได้ แต่จะสแกนได้เร็ว เน้นตรวจสอบเกี่ยวกับการตั้งค่าด้านความปลอดภัยเบื้องต้นเท่านั้น
  2. zap-full-scan.py เป็นสคริปต์ที่นอกจากทำ Passive Scan แล้วยังเน้นจัดเต็มทำ Active Scan โดยการ Fuzz ข้อมูลอันตรายในรูปแบบต่าง ๆ เข้าไปในเว็บ เพื่อหาช่องโหว่ความเสี่ยงสูงต่าง ๆ ได้ แต่จะสแกนได้ช้ากว่า แบบ Baseline ซึ่งความเร็วจะขึ้นอยู่กับจำนวนหน้าเว็บและจำนวนจุดที่รับค่าจากผู้ใช้งานเข้าไปได้
  3. zap-api-scan.py เป็นสคริปต์ที่มีวิธีใช้แตกต่างกับแบบ Baseline กับ Full Scan ตรงที่ในสองแบบแรก ผู้ใช้งานจะต้องใส่ URL ตั้งต้นของเว็บสำหรับการสแกนค้นหาช่องโหว่ แต่แบบ API Scan ผู้ใช้งานสามารถใส่ URL ไปที่ไฟล์หรือไฟล์ API Specification เช่น OpenAPI (Postman Collection), WSDL, GraphQL และ Swagger เข้ามา (ส่วนมากจะเป็นรูปแบบ JSON หรือ YAML) เพื่อทำการสแกนช่องโหว่ใน API ต่าง ๆ โดยไม่ต้องทำการสำรวจหาหน้าเว็บต่าง ๆ (Spider) นั่นเอง

ซึ่งสามารถดูคู่มือการใช้งาน OWASP ZAP Docker เบื้องต้นได้ที่ https://www.zaproxy.org/docs/docker/  และ https://github.com/zaproxy/community-scripts/tree/main/other  

1.2 เข้าสู่ระบบด้วย Context หรือ Hook (auth_hook.py)

ความท้าทายของการใช้ OWASP ZAP ในรูปแบบ Headless ด้วยสคริปต์ใน Docker ข้างต้น คือถ้าหากในเว็บนั้นจะต้องเข้าสู่ระบบก่อน จะต้องมีการตั้งค่าการสแกนเพิ่มเติม โดยใน OWASP ZAP จะมีสิ่งที่เรียกว่า Context เป็นเหมือนการตั้งค่า ว่าจะให้สแกนอะไรไม่สแกนอะไร (เช่นห้ามไปกดปุ่ม ออกจากระบบ หรือปุ่มที่กดแล้วจะลบข้อมูลในระบบทิ้ง) รวมถึงการจัดการเกี่ยวกับการเข้าสู่ระบบ ว่าจะ เข้าสู่ระบบ ตรงไหน ด้วยรหัสผ่านอะไร

โดยจะต้องใส่ Context เข้ามาเป็น Argument ซึ่งจะมีหลัก ๆ ที่เกี่ยวข้องดังนี้

-n context_file   context file which will be loaded prior to scanning the target
-U user           username to use for authenticated scans – must be defined in the given context file

ปัญหาคือการทำไฟล์ Context นั้นมีขั้นตอนซับซ้อนเล็กน้อย (เพราะต้องทำแบบ Manual ด้วย GUI ของ OWASP ZAP) สำหรับบางคนที่ต้องการอะไรเป็น Command Line Interface (CLI) ก็เลยมีคนไปทำสคริปต์ Hook ชื่อว่า auth_hook.py ที่ช่วยในการ จัดการเรื่องพวกนี้ให้ ในชื่อโครงการ zap2docker-auth จากเดิมของ Official จาก OWASP คือ zap2docker เฉย ๆ (https://github.com/ICTU/zap2docker-auth-weekly) เอาไปใช้ร่วมกับ Argument ชื่อ –hook กับ -z ของสคริปต์ใน zap2docker ปกติได้เลย

-z zap_options ZAP command line options e.g. -z “-config aaa=bbb -config ccc=ddd”
–hook            path to python file that define your custom hooks

โดยที่ zap2docker-auth จะรองรับการ ใส่ชื่อผู้ใช้งานกับรหัสผ่านไปยังหน้าเว็บหรือ API สำหรับเข้าสู่ระบบ บนเว็บ ก่อนการสแกนค้นหาช่องโหว่ รวมถึงช่วยในการจดจำค่า Session Token ที่ได้จากการเข้าสู่ระบบสำเร็จนั้น เพื่อนำไปทำการค้นหาหน้าเว็บ (Spider) และทำ Passive Scan กับ Active Scan ต่อไปนั่นเอง

ตัวอย่างคำสั่งการใช้สคริปต์ Full Scan ไปสแกนค้นหาช่องโหว่เว็บ Damn Vulnerable Web Application (DVWA) ด้วย OWASP ZAP แบบ Headless ที่มีการเข้าสู่ระบบก่อนทำการสแกนด้วยสคริปต์ auth_hook.py

./zap-full-scan.py -t http://dvwa.local/ -r Report_ZAP_DVWA_AuthN.html --hook=/zap/auth_hook.py -z "auth.loginurl=http://dvwa.local/login.php
auth.auto
auth.username='admin'
auth.password='password'
auth.username_field="username"
auth.password_field="password"
auth.auto=1
auth.submit_field="submit"
auth.exclude='.*logout.*,.*setup.*,.*security.*'
"

2. Jenkins

การทำ DevOps  มักจะใช้ร่วมกับวิธีการที่เรียกว่า CI/CD ที่ย่อมาจากคำว่า Continuous Integration และ Continuous Delivery แปลแบบเข้าใจง่าย ๆ ว่าเป็นการทำ Build และ Deploy ให้บ่อยขึ้น โดยที่ Dev ไม่ต้องคอยรอให้ Ops เอาแอปไปติดตั้งให้

2.1 CI/CD Pipeline

CI/CD มักจะต้องใช้ CI/CD Pipeline ที่เป็นกระบวนการที่เราอาจจะมีโค้ดหรือไฟล์การตั้งค่า ที่จะมาระบุ ลำดับขั้นตอนใน CI/CD ว่ามีขั้นตอนอะไรบ้าง แล้วในแต่ละขั้นตอนจะต้องทำอะไรอัตโนมัตินั้น ๆ ตัวอย่างเช่นขั้นตอนอาจจะมี

  1. ทำการไปหยิบ (git clone) โค้ดจาก Code Repository มา (ในบทความนี้เราจะใช้ GitLab)
  2. ทำการ Build โค้ดที่หยิบมา โดยอาจจะเป็นการ Compile รวมถึงขั้นตอนการดึง Software Library ที่เป็น Dependency ต่าง ๆ มาด้วย (ในบทความนี้เราจะใช้ คำสั่ง npm install)
  3. ทำการ Test โค้ดที่ Build เสร็จว่าใช้งานในฟีเจอร์ต่าง ๆ ได้ตามที่ควรจะเป็น โดยส่วนมากแล้วจะเป็น Automated Test อย่างสคริปต์ Unit Test ที่ฝากไว้รวมกับใน Code Repository
  4. ทำการ Deploy โค้ดที่ build เสร็จแล้ว เป็นไฟล์ Docker Image แล้วเอาไฟล์นั้นไปจัดเก็บไว้ใน ที่เก็บ Docker Image หรือเรามักจะเรียกกันว่า Docker Repository (ในบทความนี้เราจะใช้ Sonatype Nexus Repository ทำหน้าที่นี้กัน)

ถ้าหากเราทำ CI/CD Pipeline สำเร็จ ก็ จะช่วยให้ ทางผู้พัฒนาโปรแกรม (ทีม Development) สามารถเอาโค้ดที่เขียนไปลองติดตั้งเพื่อทดสอบใช้งานได้อย่างรวดเร็ว เห็นผลลัพธ์ได้ง่ายขึ้น (เรียกว่ามี Feedback Loop ที่ดีขึ้น และพึ่งพาทีม Operations น้อยลง)  

โดย CI/CD Pipeline อาจจะต้องมีการกำหนด ว่าจะถูกสั่งให้ทำงานเมื่อไรด้วย (Trigger) ตัวอย่างเช่น สามารถกำหนดให้ CI/CD Pipeline ทำงานหลังจากการส่งโค้ด (commit และ push) เข้าไปใน Code Repository ก็ได้ (หรือถ้าหากไม่อยาก build บ่อย ๆ ก็อาจจะกำหนด branch ไปว่า ให้ Build เฉพาะเวลา commit เข้า branch ที่เป็น main แต่ตอนพัฒนาแยกไป branch อื่นก็ได้เช่นกัน)

สำหรับ CI/CD Pipeline สามารถทำได้จากโปรแกรมทำ Automation จากหลากหลายค่าย อย่าง GitHub Actions (.github/workflows), Azure DevOps Pipelines (azure-pipelines.yml), GitLab CI/CD Pipeline (.gitlab-ci.yml) 

2.2 Jenkinsfile

แต่ในบทความนี้จะใช้ CI/CD Pipeline ของโปรแกรม อีกตัวหนึ่งที่ไม่เสียค่าใช้จ่ายและเปิดเผยโค้ดเป็นสาธารณะทีชื่อว่า Jenkins ซึ่งจะระบุขั้นตอนในกระบวนการ CI/CD เป็นไฟล์ชื่อว่า Jenkinsfile

Jenkinsfile จะมี รูปแบบ เขียนได้สองแบบคือ Scripted ด้วยโค้ดภาษา Groovy ล้วน ๆ กับแบบ Declarative ที่จะมีโครงสร้างคล้าย ๆ ไฟล์การตั้งค่า ที่แยกแต่ละขั้นตอนของ CI/CD ใน Pipeline ให้ใส่คำสั่งต่าง ๆ เข้าไป (แต่ก็ยังสามารถแทรกโค้ด Groovy เข้าไปได้อยู่)

ตัวอย่างไฟล์ Jenkinsfile ในรูปแบบ Declarative ดังภาพที่ 3

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                echo 'Building..'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing..'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying....'
            }
        }
    }
}
ภาพที่ 3

สามารถอ่านรายละเอียดเกี่ยวกับ Jenkinsfile เพิ่มเติมได้ที่ https://www.jenkins.io/doc/book/pipeline/jenkinsfile/ 

3. GitLab

GitLab เป็นโปรแกรมที่ทำหน้าที่หลัก ๆ คือเก็บและบริหารจัดการโค้ดด้วย Git รวมถึงมีเครื่องมือต่าง ๆ เช่น Issue, Merge Request และ To-Do List ผ่านทางหน้าเว็บ ดังภาพที่ 4

ภาพที่ 4

แต่บนหน้าเว็บหลัก GitLab เรียกตัวเองว่า “GitLab: The DevSecOps Platform” เพราะว่า GitLab เองก็สามารถถูกใช้เป็นส่วนประกอบหนึ่งใน CI/CD Pipeline อย่างในกรณีของบทความนี้คือ 

  • การ Trigger การทำงาน CI/CD Pipeline ด้วย GitLab Webhook โดยการตั้งค่าให้ Trigger เมื่อมีการ Push โค้ดขึ้นไปบน GitLab นั่นเอง
  • เมื่อ CI/CD Pipeline เริ่มทำงาน Jenkins ก็นำโค้ดของแอปมา Build, Test, Deploy ตามที่เราระบุไว้ใน Jenkinsfile ต่อไป

4. Nexus

Nexus หรือ Sonatype Nexus Repository เป็นโปรแกรมที่ทำหน้าที่เป็น Binary Repository Manager (BRM) ที่ใช้ในการจัดเก็บไฟล์ต่าง ๆ ในขั้นตอนการพัฒนาโปรแกรม เช่นไฟล์ Software Library จากภายนอกหรือภายมาเก็บไว้ ให้ภายในองค์กรเรียกใช้ เป็นรุ่นเดียวกัน (หรือโดยไม่ต้องออกอินเทอร์เน็ต) หรือใช้เก็บ Docker Image ที่ถูกสร้างมาจาก CI/CD Pipeline ก่อนจะเอาไป Deploy เป็น Container ก็ได้ ดังภาพที่ 5

ภาพที่ 5

5. OWASP Juice Shop

OWASP Juice Shop เป็นเว็บแอปพลิเคชัน ที่ออกแบบมาเป็นพิเศษ จงใจให้มีช่องโหว่ เพื่อจุดประสงค์ในการใช้ศึกษาเรื่องความปลอดภัยของเว็บโดยเฉพาะ ซึ่งตัวเว็บพัฒนาด้วยภาษา Node.js ในบทความนี้เราจะใช้โค้ดของ OWASP Juice Shop เป็นตัวอย่างระบบสำหรับถูก CI/CD Pipeline ของเราปู้ยี้ปู้ยำ ดังภาพที่ 6

ภาพที่ 6

สามารถดาวน์โหลดโค้ดของ OWASP Juice Shop ได้ฟรีที่ https://github.com/juice-shop/juice-shop

เริ่มทำ CI/CD Pipeline

1. มี Service อะไรบ้างใน CI/CD Pipeline

หลังจากเรา เห็นคำอธิบายส่วนประกอบต่าง ๆ กันมาเบื้องต้นแล้ว ก็ขอสรุป Service ต่าง ๆ ดังนี้

  • OWASP ZAP ใช้สำหรับสแกนค้นหาช่องโหว่
  • Jenkins ใช้สำหรับการทำ CI/CD Pipeline
  • GitLab ใช้เก็บโค้ด
  • Nexus ใช้เก็บไฟล์ Docker Image
  • Debian 11 ใช้เป็นเครื่อง Linux ที่จะไว้ Deploy เว็บ OWASP Juice Shop

เพื่อความสะดวกเราจะทดสอบ DevSecOps ฉบับย่อ ในรูปแบบของ Docker Compose ที่จะใส่ขั้นตอนของ OWASP ZAP มาสแกนค้นหาช่องโหว่ ตอนที่เว็บแอปพลิเคชัน OWASP Juice Shop กำลังทำงาน หลังจาก Deploy เสร็จแล้วกัน

คำเตือน: ตัวอย่าง Docker Compose ในบทความนี้ใช้เพื่อการทดสอบ (Proof of Concept) การทำ DevSecOps ฉบับย่อเท่านั้น ห้ามนำไปใช้งานจริงในสภาพแวดล้อมจริง (Production) เด็ดขาด ในการนำไปใช้งานจริง ควรมีการตั้งค่าความปลอดภัยอื่น ๆ เพิ่มเติม ตัวอย่างเช่น การให้ Jenkins เข้าถึง Docker Service ด้วยวิธีการไม่ Volume Unix Socket ของ Docker Daemon เข้าไปใน Container รวมถึงการหลีกเลี่ยงการใช้งาน Privileged Container ใน Service ต่าง ๆ ที่มีความเสี่ยงสูงในการถูก Sandbox Escape จาก Docker Guest มาที่ Docker Host ได้ (ถ้าหาก Service ถูกเจาะระบบ) รวมถึงการจัดการ Secret ซึ่งจะไม่ได้กล่าวถึงในบทความนี้ 

File: docker-compose.yml

version: "3.8"

services:
  jenkins:
    image: jenkins/jenkins:lts
    hostname: jenkins
    container_name: devsecops-jenkins
    restart: always
    networks:
      devsecopsnet:
        ipv4_address: 172.20.0.6
    ports:
      - "8087:8080"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
      - "jenkins_home:/var/jenkins_home"
    privileged: true
    environment:
      - "JENKINS_OPTS=--prefix=/jenkins"

  gitlab:
    image: gitlab/gitlab-ce:latest
    hostname: gitlab
    container_name: devsecops-gitlab
    restart: always
    networks:
      devsecopsnet:
        ipv4_address: 172.20.0.2
    environment:
      - GITLAB_HOST=172.20.0.2
      - GITLAB_PORT=8088
      - GITLAB_SSH_PORT=10022
    ports:
      - "8088:80"
      - "10022:22"
    volumes:
      - "gitlab_config:/etc/gitlab"
      - "gitlab_logs:/var/log/gitlab"
      - "gitlab_data:/var/opt/gitlab"

  nexus:

  image: sonatype/nexus3
    hostname: nexus
    container_name: devsecops-nexus
    restart: always
    networks:
      devsecopsnet:
        ipv4_address: 172.20.0.4
    ports:
      - "8001:8081"
    volumes:
      - "nexus_data:/nexus-data"

  deploy-target:
    image: debian:11
    hostname: deploy-target
    container_name: devsecops-deploy-target
    command: >
      bash -c "apt update
      && apt install -y docker.io openssh-server
      && useradd -rm -d /home/jenkins -s /bin/bash -g root -G sudo -u 1000 jenkins
      && echo 'jenkins:strong_password_here' | chpasswd
      && mkdir /var/run/sshd
      && /usr/sbin/sshd -D"
    networks:
      devsecopsnet:
        ipv4_address: 172.20.0.3
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    privileged: true

  zaproxy:
    image: owasp/zap2docker-stable
    hostname: zaproxy
    container_name: devsecops-zap
    user: root
    command: "tail -f /dev/null"
    networks:
      devsecopsnet:
        ipv4_address: 172.20.0.5
    volumes:
      - "/tmp/zap/wrk:/zap/wrk"
# - "C:/Users/user/Desktop/zap/wrk:/zap/wrk"

networks:

  devsecopsnet:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/24

volumes:
  jenkins_home:
  gitlab_config:
  gitlab_logs:
  gitlab_data:
  nexus_data:

ไฟล์ docker-compose.yml เอาไว้สร้าง Docker Container หลาย ๆ อันพร้อมกัน และตั้งค่าตามที่กำหนด ในบทความนี้ จะใช้เป็นรุ่น 3.8 และทำการกำหนด Service ไว้จำนวน 5 รายการ ตามแผนของพวกเรา

ข้อดีของการทำ Volume ใน Docker Compose ให้แยกไฟล์ส่วนข้อมูลออกจากไฟล์ส่วนระบบของแต่ละ Service คือ ถ้าหากเราต้องการปรับปรุงรุ่นของ Service ใด ๆ ให้เป็น Docker Image รุ่นใหม่ขึ้น (กรณีเราใช้ Tag ของ Docker Image อย่าง latest) อย่างเช่นถ้า GitLab หรือ Jenkins มีช่องโหว่ แล้วอยากจะขยับเลขรุ่นให้สูงขึ้น เราสามารถที่จะทำได้โดยไม่ให้ข้อมูลและการตั้งค่าเดิมหาย 

หมายเหตุ: แม้ว่าการปรับปรุงรุ่นย่อยมักจะ ไม่มีผลกระทบต่อการตั้งค่าและข้อมูลเดิม อย่างไรก็ตามควรมีการสำรองข้อมูลเดิม ก่อนการทดสอบปรับปรุงรุ่นทุกครั้ง ถ้าหากในกรณีที่เกิดข้อผิดพลาดจะสามารถย้อนรุ่นและข้อมูลเดิมคืนมาได้

 

$ docker compose pull
$ docker compose down
$ docker compose up -d

2. ติดตั้ง DevSecOps Pipeline

เราจะใช้ Jenkins ที่เป็น โปรแกรม Automation ที่ใช้ทำ CI/CD Pipeline ร่วมกับ OWASP ZAP แบบ Headless เพื่อทำการสแกนค้นหาช่องโหว่ทางด้านความปลอดภัยของเว็บ OWASP Juice Shop โดยมี ขั้นตอนการทำงาน ดังภาพที่ 7

ภาพที่ 7

2.1 การติดตั้งและตั้งค่าเบื้องต้น

โดยขั้นตอนแรกทำการตั้งค่า /etc/hosts (กรณีผู้อ่านใช้ macOS หรือ Linux) หรือไฟล์ C:\windows\system32\drivers\etc\HOSTS (กรณีผู้อ่านใช้ Windows) ตามด้านล่างนี้

127.0.0.1   devsecops.local 

ทำการติดตั้ง Docker Desktop และ Docker Engine ตามแต่ ระบบปฎิบัติการของผู้อ่านใช้งาน โดยสามารถดาวน์โหลดได้ที่ URL https://www.docker.com/products/docker-desktop/

ในบทความนี้ผู้เขียนจะใช้ Ubuntu 22.04 LTS เป็น Docker Host และติดตั้ง Docker Engine ด้วยคำสั่งต่อไปนี้

และเนื่องจากหลังจากสแกนค้นหาช่องโหว่เสร็จแล้ว เราจะต้องเปิดไฟล์รายงานผลช่องโหว่ (.html) ขึ้นมาอ่าน ดังนั้นใน กรณีของบทความนี้เราจะใช้ Docker Volume ในการเอาไฟล์รายงานออกมาแบบง่าย ๆ ในตัวอย่างไฟล์ docker-compose.yml จะเป็นของ Docker Host ที่เป็น Linux ถ้าหากเป็น Windows สามารถแก้ไขบรรทัด

  volumes:
      - "/tmp/zap/wrk:/zap/wrk"
# - "C:/Users/user/Desktop/zap/wrk:/zap/wrk"

เป็น

volumes:
     - "C:/Users/user/Desktop/zap/wrk:/zap/wrk"

แทนเพื่อให้นำไฟล์มาใส่ไว้ใน Directory ของ Windows (แก้ไขให้ตรงกับ Path และชื่อ Username ที่ต้องการได้เลย)

จากนั้นสร้างไฟล์ docker-compose.yml ไว้ใน Directory ที่ต้องการและใช้ Terminal หรือ cmd.exe เข้าไปที่ Directory นั้นแล้วสั่งงานด้วยคำสั่ง docker compose up -d เพื่อทำการสร้าง Docker Container ตามที่ระบุไว้ใน Docker Compose ดังภาพที่ 8 นั่นเอง

ภาพที่ 8

2.2 ตั้งค่า Nexus

เข้าไปที่ URL http://devsecops.local:8001 แล้วกดปุ่ม Sign In ด้านขวาบนจะพบข้อความว่า “Your admin user password is located in /nexus-data/admin.password on the server.”

จากนั้น ทำการอ่านรหัสผ่านเริ่มต้นของ Nexus โดยใช้คำสั่งต่อไปนี้

$ docker exec -it -u root devsecops-nexus bash
$ cat /nexus-data/admin.password
# จะได้ค่ารหัสผ่านของ admin ตัวอย่างเช่น d04b5bcd-ae1b-4f8a-8d11-a203facc1acd

จากนั้นนำรหัสผ่านไปเข้าสู่ระบบและตั้งรหัสผ่านใหม่ ดังภาพที่ 9

ชื่อผู้ใช้งาน: admin
รหัสผ่าน: ตั้งเองได้เลย

ภาพที่ 9

ทำการสร้าง Repository สำหรับเก็บ Docker Image โดยไปที่เมนู Administration > Repository หรือกดเข้าที่ URL http://devsecops.local:8001/#admin/repository/repositories

กดปุ่ม Create Repositories เลือก docker (hosted) และกำหนดรายละเอียด Repository ดังภาพที่ 10
ดังนี้

Name: docker-nexus
เลือก HTTP: 80
ทำเครื่องหมายถูกหน้า Enable Docker V1 API

ภาพที่ 10

หมายเหตุ: ในการนำไปปรับใช้งานจริงบน Production ควรใช้งานเป็น HTTPS และสร้าง TLS Certificate อย่างถูกต้อง ในบทความนี้ทำเพื่อ Proof of Concept เฉย ๆ

ต้องทำการตั้งค่า Docker Engine เพื่อให้ Jenkins ใช้ Nexus แบบ HTTP ได้ ในโปรแกรม Docker Desktop (Windows และ MacOS) ไปที่ Docker Desktop > Settings > Docker Engine Apply & Restart

โดยเพิ่มค่า หมายเลขไอพีของ Nexus ที่จะเป็น Docker Repository แบบ HTTP ต่อท้าย ดังภาพที่ 11

"insecure-registries": [
  "172.20.0.4"
]
ภาพที่ 12

2.3 ตั้งค่า GitLab

ทำการสร้างค่ารหัสผ่านของ GitLab โดยใช้คำสั่งต่อไปนี้

$ docker exec -it -u root devsecops-gitlab bash
$ gitlab-rake 'gitlab:password:reset[root]'
# ตั้งรหัสผ่านต้องมีความยาวอย่างน้อย 8 ตัวอักษร
Enter password: 
Confirm password: 
Password successfully updated for user with username root.

จากนั้นไปที่ URL http://devsecops.local:8088/users/sign_in และเข้าสู่ระบบด้วย ชื่อผู้ใช้งาน root กับรหัสผ่าน ตามที่ได้ทำการ สร้างไป

ชื่อผู้ใช้งาน: root
รหัสผ่าน: ตั้งเองได้เลย

ไปที่ URL http://devsecops.local:8088/admin/application_settings/general เมนู Import and export settings กด Expand มองหา Import sources แล้วทำเครื่องหมายถูกหน้า Repository by URL แล้วกด Save changes ดังภาพที่ 12

ภาพที่ 12

ทำการสร้างโครงการใหม่ โดยไปที่ URL http://devsecops.local:8088/projects/new และเลือก Import Project และกดปุ่ม Repository URL ใส่ URL ของ OWASP Juice Shop เข้าไปโล้ด   https://github.com/juice-shop/juice-shop.git กำหนด namespace เช่น root แล้วกดปุ่ม Create project ดังภาพที่ 13

ภาพที่ 13

หลังจากได้โค้ดของ OWASP Juice Shop เสร็จแล้ว ไปทำการสร้าง Access Token เพื่อให้ Jenkins สามารถเข้ามาเรียกใช้งาน GitLab ได้ โดยไปที่ URL http://devsecops.local:8088/root/juice-shop/-/settings/access_tokens
กดปุ่ม Add new token และกำหนดรายละเอียด Access Token ดังภาพที่ 14 ดังนี้

Token name: devsecops
Role: Guest
Scope: api

ภาพที่ 14

2.4 ตั้งค่า Jenkins

เนื่องจากในตัวอย่างบทความนี้เราจะใช้ Jenkins ช่วย Build สร้าง Docker Image และ Push ขึ้นไปยัง Nexus ดังนั้นเราเลยจะทำการ ติดตั้ง Docker Engine ใน Docker Container ของ Jenkins

$ docker exec -it -u root devsecops-jenkins bash
$ apt update && apt install -y docker.io

สำหรับการเข้าสู่ระบบในเว็บ Jenkins ที่ URL http://devsecops.local:8087/jenkins/
จะใช้ รหัสผ่าน สำหรับการเข้าสู่ระบบครั้งแรก ของ Jenkins โดยใช้คำสั่งด้านล่างนี้

$ cat /var/jenkins_home/secrets/initialAdminPassword
# จะได้รหัสผ่านสำหรับเข้าสู่ระบบเว็บ Jenkins

ไปที่ URL http://devsecops.local:8087/jenkins/manage/pluginManager/available ทำการ ติดตั้ง Plugin ของ Jenkins ได้แก่ Command Agent Launcher Plugin, Oracle Java SE Development Kit Installer Plugin, SSH Pipeline Steps, SSH plugin, SSH server, GitLab Plugin และ Docker Pipeline ดังภาพที่ 15

ภาพที่ 15

จากนั้นตั้งค่าให้ Jenkins สามารถใช้งานร่วม กับ GitLab ได้ โดยไปที่ URL http://devsecops.local:8087/jenkins/manage/configure เลือก Credentials: Add > Jenkins > Kind: GitLab API token จากนั้นทำการเพิ่ม API Token ที่สร้างมาจาก GitLab ดังภาพที่ 16

ภาพที่ 16

ตั้งชื่อ Connection Name: GitLab – DevSecOps
GitLab Host URL: http://devsecops-gitlab
และระบุ API token ที่ได้จาก GitLab ในขั้นตอนก่อนหน้า (glpat-…)
(เป็นชื่อ hostname ที่ Docker Container ใช้คุยกันเอง จะไม่เหมือนชื่อที่เราตั้งไว้ในไฟล์ hosts) ดังภาพที่ 17

ภาพที่ 17

เลื่อนลงมาจะพบกับการตั้งค่า Declarative Pipeline (Docker) ให้ระบุค่าดังนี้ และทำการกด Save ดังภาพที่ 18

Docker Label: Nexus – DevSecOps
Docker registry URL: http://devsecops-nexus:8002/

Registry credentials: Add > Jenkins
Kind: Username with password
Password: {{nexus password}}
ID: nexus-userpass

ภาพที่ 18

ทำการสร้าง Pipeline ไปที่เมนู New Item > Pipeline ดังภาพที่ 19

ภาพที่ 19

ทำเครื่องหมายถูกหน้า Discard old builds ใส่ค่า Max # of builds to keep เป็น 3 ดังภาพที่ 20

ภาพที่ 20

ในส่วน GitLab Connection ตั้งค่าเป็น DevSecOps ตรง Build Triggers ทำการทำเครื่องหมายถูกหน้า Build when a change is pushed to GitLab ดังภาพที่ 21

ภาพที่ 21

จากนั้นไปที่เมนู Advanced > Secret token > Generate เพื่อทำการสร้างค่า Secret Token ดังภาพที่ 22

ภาพที่ 22

ตั้งค่า Pipeline โดยไปที่เมนู Pipeline > Definition > Pipeline script from SCM แล้วทำการตั้งค่าตามด้านล่างนี้แล้วกด Save ดังภาพที่ 23

SCM: Git
Repository URL: http://devsecops-gitlab/root/juice-shop.git
Credentials: Add > Jenkins

  • Kind: Username with password
  • Username: root
  • Password: ใส่ค่า Access Token จาก GitLab ในขั้นตอนก่อนหน้า
    Script Path: Jenkinsfile
ภาพที่ 23

2.5 ตั้งค่า Webhook

กลับมาในส่วนของ GitLab เราจะทำการตั้งค่า Webhook ไว้เพื่อให้ GitLab เรียก Webhook ของ Jenkins เพื่อบอกว่ามี Push เข้ามาแล้ว จากนั้น Jenkins จะทำการเริ่ม Build โดยเริ่มจากไป Pull โค้ด ที่อยู่ใน GitLab

จะทำการตั้งค่าให้ไปที่ เมนู Admin > Settings > Network URL: http://devsecops.local:8088/admin/application_settings/network และทำเครื่องหมายถูกหน้า Allow requests to the local network from webhooks and integrations ดังภาพที่ 24

ภาพที่ 24

จากนั้นมาที่เมนู Project > Settings แล้วทำการตั้งค่าตามด้านล่างนี้แล้วกด Add webhook ดังภาพที่ 25

URL: http://devsecops-jenkins:8080/jenkins/project/juice-shop
Secret token: ค่า Secret Token จาก Jenkins ในขั้นตอนก่อนหน้า
Trigger:

  • Push events
  • Merge request events
ภาพที่ 25

3. Jenkins Pipeline

File: Jenkinsfile

pipeline {
    agent any

    environment {
      SSH_CREDENTIALS = credentials('ssh-userpass')
      NEXUS_CREDENTIALS = credentials('nexus-userpass')
      NEXUS_HOST = '172.20.0.4'
      DEPLOY_PORT = 8000
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build image') {
            steps {
                updateGitlabCommitStatus name: 'build', state: 'running'
                echo 'Starting to build docker image'

                script {
                    docker.withRegistry("http://${env.NEXUS_HOST}/", 'nexus-userpass') {
                        def juiceShopImage = docker.build("owasp/juice-shop:${env.BUILD_ID}")
                        juiceShopImage.push()
                    }
                }
            }
        }

        stage('Deploy') {
            steps {
                echo 'Deploying to target'

                script {
                    def remote = [:]
                    remote.name = 'deploy-target'
                    remote.host = 'devsecops-deploy-target'
                    remote.user = env.SSH_CREDENTIALS_USR
                    remote.password = env.SSH_CREDENTIALS_PSW
                    remote.allowAnyHosts = true
                    sshCommand remote: remote, command: "docker login -u ${env.NEXUS_CREDENTIALS_USR} -p ${env.NEXUS_CREDENTIALS_PSW} http://${env.NEXUS_HOST}/"
                    sshCommand remote: remote, command: "docker pull ${env.NEXUS_HOST}/owasp/juice-shop:${env.BUILD_ID}"
                    sshCommand remote: remote, command: "docker rm -f juice-shop 2>/dev/null"
                    sshCommand remote: remote, command: "docker run -dp ${env.DEPLOY_PORT}:3000 --name juice-shop ${env.NEXUS_HOST}/owasp/juice-shop:${env.BUILD_ID}"
                }
            }
        }

        stage('OWASP ZAP Scan') {
            steps {
                echo 'Scanning target with OWASP ZAP'

                script {
                    try {
                        sh "docker exec devsecops-zap zap-full-scan.py -t http://172.20.0.1:${env.DEPLOY_PORT} -a -r zapreport-${env.BUILD_ID}.html"
                    } catch (err) {
                        echo err.getMessage()
                    }
                }
            }
        }
    }
    post {
      failure {
        updateGitlabCommitStatus name: 'build', state: 'failed'
      }
      success {
        updateGitlabCommitStatus name: 'build', state: 'success'
      }
    }
}

ในส่วนของ Pipeline มีรายละเอียดตามด้านล่างนี้

โดยส่วนแรก เป็นส่วนของ syntax Pipeline

pipeline {
}

จากนั้นกำหนด agent เป็น any คือ ใช้ execute อันไหนก็ได้ในการ run stages ทั้งหมด

agent any

ถัดมาเป็นส่วนของ environment เป็น environment ที่จะใช้ในการ run Pipeline ในที่นี้เป็น Nexus

environment {
      SSH_CREDENTIALS = credentials('ssh-userpass')
      NEXUS_CREDENTIALS = credentials('nexus-userpass')
      NEXUS_HOST = '172.20.0.4'
      DEPLOY_PORT = 8000
    }

และในส่วนของ stage และ steps
stage เป็นการทำงานในแต่ละส่วนของ Pipeline โดยในบทความนี้มี 4 stage ก็คือ 1.Checkout 2. Build image 3. Deploy 4. OWASP ZAP Scan

stage('Checkout') {
}

ส่วน steps เป็นการทำงานในส่วนของ stages อีกทีเพื่อทำงานในส่วนของ stages นั้น ๆ

steps {
     checkout scm
}

ส่วนสุดท้ายของ Pipeline ก็คือ post เอาไว้กำหนดขั้นตอนเพิ่มอีกขั้นตอนหนึ่งหลังจากที่ Pipeline ทำการ run stage ด้านบนเสร็จสิ้นเพื่อดูผลลัพธ์ ของการ run Pipeline ว่า failure หรือ success

post {
      failure {
        updateGitlabCommitStatus name: 'build', state: 'failed'
      }
      success {
        updateGitlabCommitStatus name: 'build', state: 'success'
      }
    }
}

4. ทดสอบรัน Pipeline

Run คำสั่ง git clone ในฝั่งของ local host ดังภาพที่ 26

$ git clone http://devsecops.local:8088/root/juice-shop.git

จากนั้นทำการเพิ่ม example.txt และ Run คำสั่ง git push

$ echo "DevSecOps" | tee example.txt 
$ git add example.txt 
$ git commit -m "Add example.txt"
$ git push
ภาพที่ 26

5. ตัวอย่างผลการสแกนช่องโหว่

ตรวจสอบผลสแกน $open ./zapreport-25.html ดังภาพที่ 27

ภาพที่ 27

จะพบว่ามีช่องโหว่ที่เป็น Medium 2 ช่องโหว่ Low 4 ช่องโหว่ Informational 5 ช่องโหว่

ข้อควรระวังสำหรับการใช้งาน OWASP ZAP ในการสแกน

  • การสแกน โดยใช้ OWASP ZAP ที่เป็นโปรแกรมสแกนอัตโนมัติ จะไม่ได้ผลการทดสอบที่ละเอียดเท่ากับการทำ Pentest โดย OWASP ZAP จะทำการสแกนช่องโหว่ที่เกิดจากการตั้งค่าไม่ปลอดภัยเบื้องต้นเท่านั้น
  • ถ้าไม่ได้มีการตั้งค่าโปรแกรม OWASP ZAP ให้ทำการสแกนในส่วนของฟังก์ชันที่ต้องเข้าสู่ระบบก่อน ฟังก์ชันนั้นก็จะไม่ได้ถูกทดสอบด้วย
  • อาจจะมีผลการทดสอบที่เป็นค่า False Positive เช่น โปรแกรม OWASP ZAP สแกนเจอช่องโหว่แต่จริง ๆ แล้วไม่มีช่องโหว่ หรือ False Negative คือ โปรแกรม OWASP ZAP สแกนไม่เจอช่องโหว่ แต่อาจจะมีช่องโหว่นั้นอยู่
  • Risk Rating หรือค่าความเสี่ยงที่ได้จากการสแกน อาจจะไม่ตรงกับความเป็นจริง

หลังจากผู้อ่านได้เห็นวิธีการทำ DevSecOps ด้วย OWASP ZAP ก็หวังว่าจะได้ความรู้กันหลายอย่าง ไม่ว่าจะเป็นเรื่องการทำ DevSecOps ในการนำหลักคิดและโปรแกรมของการสร้าง Security เข้ามาอยู่ในทุกขั้นตอนของกระบวนการทำ DevOps และการสแกน OWASP ZAP แบบ Headless ที่ช่วยให้สามารถใช้กับ Script หรือคำสั่งควบคุมได้โดยไม่ต้องใช้ GUI

บทความที่เกี่ยวข้อง