GitLab ile Container Security Scanning Nasıl Yapılır

Containerlarla çalışıyorsanız, güvenlik açıklarını production’a taşımadan önce yakalamak hayati önem taşıyor. GitLab’ın yerleşik Container Security Scanning özelliği, tam da bu iş için tasarlanmış. Pipeline’ınıza birkaç satır YAML ekleyerek Docker imajlarınızı otomatik olarak taratabilir, CVE veritabanlarına karşı kontrol edebilir ve güvenlik açıklarını merge request aşamasında görünür hale getirebilirsiniz. Bu yazıda, gerçek dünya senaryolarıyla bu sistemi nasıl kuracağınızı ve ince ayarlarını nasıl yapacağınızı ele alacağız.

Container Security Scanning Nedir ve Neden Önemlidir?

Container imajları, onlarca hatta yüzlerce paket ve bağımlılık içerir. Ubuntu 22.04 base image’ından başladığınızda, içinde zaten yüzlerce paket geliyor. Bunların bir kısmında güvenlik açıkları olabilir. Siz üstüne Node.js, Python veya Java bağımlılıklarını eklediğinizde bu sayı daha da büyüyor.

GitLab Container Scanning, Trivy ve Grype gibi araçları arka planda kullanarak imajınızdaki:

  • OS seviyesindeki paket açıklarını (apt, yum, apk ile kurulanlar)
  • Dil bazlı bağımlılık açıklarını (npm, pip, gem, Maven)
  • Konfigürasyon sorunlarını (yanlış izinler, root user kullanımı)

otomatik olarak tespit eder ve raporlar.

GitLab Ultimate lisansı kullanıyorsanız bu raporlar doğrudan Security Dashboard’da görünür. Free veya Premium kullanıyorsanız da artifact olarak JSON raporu alırsınız, sadece UI entegrasyonu eksik olur.

Önkoşullar ve Hazırlık

Başlamadan önce ortamınızın hazır olduğundan emin olalım.

İhtiyacınız olanlar:

  • GitLab 14.0 veya üzeri (self-hosted veya GitLab.com)
  • Docker-in-Docker (DinD) desteği olan bir GitLab Runner veya Kubernetes executor
  • Registry’e push edilmiş ya da pipeline içinde build edilen bir container imajı
  • GitLab Runner’ın privileged: true modda çalışıyor olması (DinD için)

Runner konfigürasyonunuzu kontrol etmek için:

# Runner config dosyasını kontrol et
cat /etc/gitlab-runner/config.toml

# Şuna benzer bir şey görmelisiniz:
# [[runners]]
#   [runners.docker]
#     privileged = true

Eğer runner’ınız privileged modda değilse, Kubernetes executor veya socket binding yöntemi de kullanılabilir. Ancak en kolay başlangıç DinD ile oluyor.

Temel Pipeline Kurulumu

En basit haliyle container scanning eklemek için .gitlab-ci.yml dosyanıza şunu eklemeniz yeterli:

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

variables:
  CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

stages:
  - build
  - test
  - scan

build:
  stage: build
  image: docker:24.0
  services:
    - docker:24.0-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

container_scanning:
  stage: scan
  needs:
    - build

Bu kadar. GitLab, container_scanning job’unu tanıyarak şablonu otomatik olarak uygular. CS_IMAGE değişkeni, hangi imajın taranacağını belirtir.

Gelişmiş Konfigürasyon

Gerçek dünyada birden fazla servisiniz olduğunda ve farklı tarama ihtiyaçlarınız olduğunda konfigürasyonu genişletmeniz gerekiyor.

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

variables:
  # Tarayıcı seçimi: trivy veya grype
  CS_ANALYZER: "trivy"
  # Sadece kritik ve yüksek açıkları raporla
  CS_SEVERITY_THRESHOLD: "HIGH"
  # Registry timeout
  CS_REGISTRY_INSECURE: "false"
  # Trivy'nin kendi cache'ini kullanması için
  TRIVY_NO_PROGRESS: "true"
  TRIVY_CACHE_DIR: ".trivycache/"

container_scanning:
  stage: scan
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE/api:$CI_COMMIT_SHORT_SHA
    GIT_STRATEGY: fetch
  cache:
    paths:
      - .trivycache/
  artifacts:
    when: always
    paths:
      - gl-container-scanning-report.json
    reports:
      container_scanning: gl-container-scanning-report.json

Burada dikkat edilmesi gereken birkaç nokta var. CS_SEVERITY_THRESHOLD değişkeni ile sadece belirli bir önem seviyesinin üzerindeki açıkları raporlayabilirsiniz. LOW ve MEDIUM açıkları noise olarak değerlendiriyorsanız bu ayar çok işe yarıyor. Trivy cache’ini aktifleştirmek ise tarama sürelerini ciddi ölçüde kısaltıyor. İlk taramadan sonra vulnerability veritabanı cache’leniyor ve sonraki taramalar çok daha hızlı tamamlanıyor.

Çoklu Servis Taraması

Mikroservis mimarisinde genellikle tek bir repo’dan birden fazla imaj build etmeniz gerekir. İşte bunun için bir yaklaşım:

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

stages:
  - build
  - scan
  - report

.build_template: &build_template
  stage: build
  image: docker:24.0
  services:
    - docker:24.0-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

build:api:
  <<: *build_template
  script:
    - docker build -t $CI_REGISTRY_IMAGE/api:$CI_COMMIT_SHORT_SHA ./services/api
    - docker push $CI_REGISTRY_IMAGE/api:$CI_COMMIT_SHORT_SHA

build:worker:
  <<: *build_template
  script:
    - docker build -t $CI_REGISTRY_IMAGE/worker:$CI_COMMIT_SHORT_SHA ./services/worker
    - docker push $CI_REGISTRY_IMAGE/worker:$CI_COMMIT_SHORT_SHA

scan:api:
  extends: container_scanning
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE/api:$CI_COMMIT_SHORT_SHA
  needs:
    - build:api

scan:worker:
  extends: container_scanning
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE/worker:$CI_COMMIT_SHORT_SHA
  needs:
    - build:worker

extends: container_scanning kullanımı burada kritik. Bu şekilde GitLab’ın template’inden gelen tüm konfigürasyonu miras alırken sadece CS_IMAGE değişkenini override ediyorsunuz.

Güvenlik Açığı Yönetimi ve Allowlist

Her güvenlik açığı anında düzeltilmesi gereken bir şey değil. Bazı açıklar için upstream paket henüz güncelleme yayınlamamış olabilir, bazıları ise sizin use case’inizde istismar edilebilir nitelikte olmayabilir. Bu durumlar için GitLab’ın vulnerability exception mekanizmasını kullanabilirsiniz.

Repo’nuzun kök dizininde .gitlab klasörü altında vulnerability-allowlist.yml dosyası oluşturun:

# .gitlab/vulnerability-allowlist.yml
generalallowlist:
  CVE-2023-1234:
    comment: "Upstream fix henüz yok, next sprint'te base image güncellenecek"
  CVE-2022-5678:
    comment: "Bu açık sadece GUI gerektiriyor, containerda GUI yok"

images:
  # Sadece belirli imajlar için allowlist
  - registry.example.com/myapp/legacy-worker:
      - CVE-2021-9999:
          comment: "Legacy servis, Q3'te rewrite edilecek"

Bu dosya otomatik olarak algılanır ve listelediğiniz CVE’ler tarama raporunda “dismissed” olarak işaretlenir.

Fail Politikası Belirleme

Pipeline’ın ne zaman başarısız sayılacağını belirlemek, güvenlik olgunluğunuzun önemli bir parçası. Çok sert bir politika developer’ları bunaltırken, çok gevşek bir politika da sistemi anlamsız kılar.

include:
  - template: Security/Container-Scanning.gitlab-ci.yml

variables:
  CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

container_scanning:
  stage: scan
  # Critical açık varsa pipeline'ı durdur
  variables:
    CS_SEVERITY_THRESHOLD: "CRITICAL"
  # Job başarısız olursa pipeline da başarısız olsun
  allow_failure: false

# Ayrı bir manuel inceleme adımı ekleyin
security_review:
  stage: .post
  script:
    - echo "Güvenlik raporu incelendi ve onaylandı"
  when: manual
  needs:
    - container_scanning
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Burada bir best practice paylaşayım. Feature branch’lerde allow_failure: true kullanıp sadece main/master branch’e merge sırasında strict policy uygulamak, hem developer deneyimini iyileştirir hem de güvenliği korur.

container_scanning:
  stage: scan
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  rules:
    # Feature branch: başarısız olsa da pipeline devam etsin
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      allow_failure: true
    # Main branch: kritik açık varsa dur
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      variables:
        CS_SEVERITY_THRESHOLD: "CRITICAL"
      allow_failure: false

Özel Registry ve Air-Gap Ortamları

Şirket ortamlarında genellikle internet erişimi kısıtlıdır. Bu durumda Trivy’nin vulnerability veritabanını ve scanner imajını özel registry’nizden çekmeniz gerekir.

variables:
  # Scanner imajını kendi registry'nizden çek
  CS_ANALYZER_IMAGE: "registry.internal.company.com/security/container-scanning:6"
  # Trivy veritabanı için özel mirror
  TRIVY_DB_REPOSITORY: "registry.internal.company.com/aquasecurity/trivy-db:2"
  # Private registry authentication
  CS_REGISTRY_USER: $INTERNAL_REGISTRY_USER
  CS_REGISTRY_PASSWORD: $INTERNAL_REGISTRY_PASSWORD

container_scanning:
  stage: scan
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    # CA sertifikası doğrulamasını yönet
    TRIVY_INSECURE: "false"
    SSL_CERT_FILE: "/etc/ssl/certs/company-ca.crt"

Air-gap ortamı için Trivy veritabanını periyodik olarak indirip kendi registry’nize push etmeniz gerekiyor. Bunun için bir cron job oluşturabilirsiniz:

#!/bin/bash
# update-trivy-db.sh
# Bu script'i haftalık cron olarak çalıştırın

set -euo pipefail

INTERNAL_REGISTRY="registry.internal.company.com"
TRIVY_DB_IMAGE="aquasecurity/trivy-db"
TAG="2"

echo "Trivy DB güncelleniyor..."

# Public registry'den çek
docker pull "ghcr.io/${TRIVY_DB_IMAGE}:${TAG}"

# Internal registry'e tag'le
docker tag 
  "ghcr.io/${TRIVY_DB_IMAGE}:${TAG}" 
  "${INTERNAL_REGISTRY}/aquasecurity/${TRIVY_DB_IMAGE##*/}:${TAG}"

# Push et
docker push "${INTERNAL_REGISTRY}/aquasecurity/${TRIVY_DB_IMAGE##*/}:${TAG}"

echo "Trivy DB güncelleme tamamlandı: $(date)"

Raporları Analiz Etme ve Aksiyon Alma

Pipeline çalıştıktan sonra gl-container-scanning-report.json artifact’ı oluşur. Bu JSON’ı parse ederek kendi bildirim sistemlerinize entegre edebilirsiniz. Örneğin kritik açıklar için Slack bildirimi gönderebilirsiniz:

#!/bin/bash
# parse-security-report.sh

REPORT_FILE="gl-container-scanning-report.json"
SLACK_WEBHOOK_URL="${SLACK_SECURITY_WEBHOOK}"

if [ ! -f "$REPORT_FILE" ]; then
  echo "Rapor dosyası bulunamadı!"
  exit 1
fi

# Kritik açık sayısını bul
CRITICAL_COUNT=$(jq '[.vulnerabilities[] | select(.severity == "Critical")] | length' "$REPORT_FILE")
HIGH_COUNT=$(jq '[.vulnerabilities[] | select(.severity == "High")] | length' "$REPORT_FILE")

echo "Kritik açıklar: $CRITICAL_COUNT"
echo "Yüksek açıklar: $HIGH_COUNT"

# Slack bildirimi gönder
if [ "$CRITICAL_COUNT" -gt "0" ]; then
  SLACK_MESSAGE=$(cat <<EOF
{
  "text": ":rotating_light: *Güvenlik Uyarısı*",
  "attachments": [{
    "color": "danger",
    "fields": [
      {"title": "Proje", "value": "${CI_PROJECT_NAME}", "short": true},
      {"title": "Branch", "value": "${CI_COMMIT_BRANCH}", "short": true},
      {"title": "Kritik Açıklar", "value": "${CRITICAL_COUNT}", "short": true},
      {"title": "Yüksek Açıklar", "value": "${HIGH_COUNT}", "short": true},
      {"title": "Pipeline", "value": "${CI_PIPELINE_URL}", "short": false}
    ]
  }]
}
EOF
)
  curl -s -X POST 
    -H 'Content-type: application/json' 
    --data "$SLACK_MESSAGE" 
    "$SLACK_WEBHOOK_URL"
fi

Bu script’i pipeline’ınıza ekleyin:

notify:security:
  stage: .post
  image: alpine:3.18
  before_script:
    - apk add --no-cache curl jq
  script:
    - chmod +x ./scripts/parse-security-report.sh
    - ./scripts/parse-security-report.sh
  needs:
    - container_scanning
  artifacts:
    when: always
    paths:
      - gl-container-scanning-report.json
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Scheduled Tarama ile Sürekli Güvenlik İzleme

Container imajlarınızı bir kez tarattınız ve temiz çıktı aldınız diyelim. Ama 3 ay sonra yeni CVE’ler yayınlandı. Bunları nasıl yakalayacaksınız?

GitLab’ın Scheduled Pipelines özelliği burada devreye giriyor. Production’da çalışan imajları düzenli aralıklarla yeniden tarayabilirsiniz.

# production-scan.yml - Ayrı bir pipeline dosyası olabilir
include:
  - template: Security/Container-Scanning.gitlab-ci.yml

stages:
  - scan

variables:
  CS_ANALYZER: "trivy"
  CS_SEVERITY_THRESHOLD: "HIGH"

# Production imajlarını periyodik tara
scan:production:api:
  extends: container_scanning
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE/api:latest
    CS_REGISTRY_USER: $CI_REGISTRY_USER
    CS_REGISTRY_PASSWORD: $CI_REGISTRY_PASSWORD
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"

scan:production:worker:
  extends: container_scanning
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE/worker:latest
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"

GitLab UI’ından veya API üzerinden schedule oluşturun:

# GitLab API ile scheduled pipeline oluştur
curl --request POST 
  --header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" 
  --form "description=Haftalık Security Scan" 
  --form "ref=main" 
  --form "cron=0 2 * * 1" 
  --form "cron_timezone=Europe/Istanbul" 
  --form "active=true" 
  "https://gitlab.example.com/api/v4/projects/${PROJECT_ID}/pipeline_schedules"

Bu şekilde her Pazartesi sabah 02:00’de production imajlarınız otomatik olarak taranır ve yeni CVE’ler varsa haberdar olursunuz.

Performans Optimizasyonu

Büyük imajları tararken tarama süresi 10-15 dakikayı bulabilir. Bunu optimize etmek için birkaç yöntem var.

İlk olarak cache kullanımı en büyük etkiyi yaratır. Trivy veritabanı her taramada indirilmemeli:

container_scanning:
  stage: scan
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    TRIVY_CACHE_DIR: "/tmp/trivy-cache"
  cache:
    key: trivy-db-$CI_RUNNER_ID
    paths:
      - /tmp/trivy-cache
    policy: pull-push

İkinci olarak, tüm branch’lerde full tarama yapmak yerine sadece değişen layer’ları tarayabilirsiniz. Bu özellikle multi-stage build’lerde etkili:

container_scanning:
  rules:
    # Draft MR'larda tarama
    - if: $CI_MERGE_REQUEST_TITLE =~ /^Draft:/
      when: manual
    # Normal MR'larda otomatik tara
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: always
    # Main branch'te her zaman tara
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: always

Gerçek Dünya Senaryo: Node.js API Servisi

Hepsini bir araya getireceğimiz gerçekçi bir senaryo. Bir Node.js API servisi için eksiksiz pipeline:

# .gitlab-ci.yml
include:
  - template: Security/Container-Scanning.gitlab-ci.yml

stages:
  - build
  - test
  - scan
  - deploy

variables:
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  LATEST_TAG: $CI_REGISTRY_IMAGE:latest

build:image:
  stage: build
  image: docker:24.0
  services:
    - docker:24.0-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - |
      docker build 
        --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') 
        --build-arg VCS_REF=$CI_COMMIT_SHA 
        --cache-from $LATEST_TAG 
        -t $IMAGE_TAG 
        -t $LATEST_TAG 
        .
    - docker push $IMAGE_TAG
    - docker push $LATEST_TAG
  rules:
    - if: $CI_COMMIT_BRANCH
    - if: $CI_MERGE_REQUEST_IID

container_scanning:
  stage: scan
  variables:
    CS_IMAGE: $IMAGE_TAG
    CS_ANALYZER: "trivy"
    CS_SEVERITY_THRESHOLD: "HIGH"
    TRIVY_CACHE_DIR: ".trivycache/"
  cache:
    key: "trivy-$CI_PROJECT_ID"
    paths:
      - .trivycache/
  needs:
    - build:image
  allow_failure: false
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      variables:
        CS_SEVERITY_THRESHOLD: "CRITICAL"
    - if: $CI_MERGE_REQUEST_IID
      allow_failure: true

deploy:production:
  stage: deploy
  script:
    - echo "Deployment başlıyor..."
    - kubectl set image deployment/api api=$IMAGE_TAG
  needs:
    - container_scanning
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual

Sonuç

GitLab Container Security Scanning, birkaç satır YAML ile ekleyebileceğiniz ama organizasyonunuzun güvenlik olgunluğunu önemli ölçüde artıran bir araç. Bu yazıda ele aldıklarımızı özetleyelim.

Temel kurulum için sadece şablonu include edip CS_IMAGE değişkenini tanımlamak yeterli. Oradan itibaren ihtiyacınıza göre katman katman özellik ekleyebilirsiniz. Çoklu servis taraması, allowlist yönetimi, fail politikaları ve scheduled taramalar aşamaları geçtikçe güvenlik önlemleriniz olgunlaşır.

En kritik tavsiyelere gelince: Scheduled pipeline’ları mutlaka kurun çünkü bir kez taramak yeterli değil. Cache kullanımını ihmal etmeyin çünkü her pipeline’da veritabanı indirmek hem yavaş hem de kaynak israfı. Severity threshold’u iş ihtiyaçlarınıza göre ayarlayın, her LOW açık için pipeline durdurmak developer’ları yıldırır. Ve allowlist mekanizmasını doğru kullanın; gerçekten kabul edilen riskleri belgeleyerek dismiss edin, yoksa allowlist bir görmezden gelme listesine döner.

Container güvenliği shift-left yaklaşımının en somut uygulamalarından biri. Açığı production’da bulmak yerine pipeline’da bulmak, hem maliyeti düşürür hem de geceleri rahat uyumanızı sağlar.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir