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: truemodda ç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.
