سلامة البناء والبناء القابل للتكرار: دليل عملي لـ CI/CD

مقدمة

إذا لم تتمكن من إعادة إنتاج عملية بناء، فلن تتمكن من التحقق منها. هذه الحقيقة البسيطة تقع في صميم أمان سلسلة توريد البرمجيات. تضمن سلامة البناء (build integrity) أن ما تنشره هو بالضبط ما كنت تنوي بناءه — لا شيء مضاف، لا شيء معدّل، لا شيء تم التلاعب به بين الكود المصدري والمنتج النهائي في بيئة الإنتاج.

في السنوات الأخيرة، أثبتت هجمات سلسلة التوريد أن عملية البناء نفسها هي هدف عالي القيمة. يمكن للمهاجمين الذين يخترقون خط أنابيب البناء (build pipeline) حقن كود خبيث في برمجيات موثوقة، مما يؤثر على ملايين المستخدمين النهائيين. الدفاع الوحيد الموثوق هو جعل عمليات البناء قابلة للتكرار (reproducible builds): بالنظر إلى نفس المدخلات، يجب أن تحصل دائمًا على نفس المخرجات. عندما يتحقق هذا الضمان، يصبح أي انحراف قابلاً للاكتشاف.

يستعرض هذا الدليل مبادئ سلامة البناء والبناء القابل للتكرار، ويشرح أهميتها لأمان CI/CD، ويقدم تقنيات عملية — من تثبيت التبعيات إلى أنظمة البناء المحكمة (hermetic build systems) — يمكنك تبنيها تدريجيًا في خطوط الأنابيب الخاصة بك.

ما هي سلامة البناء؟

سلامة البناء هي الضمان بأن مدخلات البناء تنتج مخرجات البناء بشكل حتمي. بمعنى آخر، إذا أخذت نفس الكود المصدري، ونفس التبعيات، ونفس سلسلة الأدوات، ونفس تعليمات البناء، يجب أن تحصل على منتج مطابق بت-لكل-بت في كل مرة، بغض النظر عن متى أو أين تقوم بتشغيل عملية البناء.

لماذا تُعد عمليات البناء غير القابلة للتكرار خطرًا أمنيًا

عندما لا تكون عمليات البناء قابلة للتكرار، تفقد القدرة على التحقق منها. إذا أنتجت عمليتا بناء من نفس المصدر ملفات ثنائية مختلفة، كيف تعرف أيهما صحيح؟ كيف تكتشف ما إذا كان مهاجم قد حقن كودًا أثناء عملية البناء؟ ببساطة، لا تستطيع. عدم القابلية للتكرار يخلق ضبابية يستغلها المهاجمون.

خذ بعين الاعتبار مشكلة التحقق: يريد مدقق تأكيد أن ملفًا ثنائيًا مُصدرًا يتوافق مع كوده المصدري المنشور. يقوم بسحب الإيداع (commit) الموسوم، ويشغل عملية البناء، ويقارن. إذا لم يكن البناء قابلاً للتكرار، ستختلف المخرجات — ولن يكون لدى المدقق طريقة للتمييز بين اختلاف مشروع (ناتج عن طابع زمني أو ترتيب عشوائي) وبين تعديل خبيث.

العلاقة مع مستويات بناء SLSA

يعالج إطار عمل SLSA (Supply-chain Levels for Software Artifacts) سلامة البناء مباشرة من خلال مستويات مسار البناء الخاصة به:

  • SLSA Build L1: عملية البناء موثقة وتنتج بيانات وصفية للمصدر (provenance metadata).
  • SLSA Build L2: يعمل البناء على خدمة مستضافة تولد بيانات مصدر موثقة (authenticated provenance).
  • SLSA Build L3: بيئة البناء محصنة ومعزولة ومقاومة للتلاعب — حتى من قبل مشرفي المشروع.

تكمل عمليات البناء القابلة للتكرار إطار SLSA من خلال توفير آلية تحقق مستقلة. حتى لو كنت تثق بخدمة البناء (L2/L3)، فإن القابلية للتكرار تتيح لأي شخص إعادة البناء من المصدر والتحقق من تطابق المخرجات.

أمثلة من الواقع

SolarWinds (2020): اخترق المهاجمون نظام بناء SolarWinds وحقنوا بابًا خلفيًا (backdoor) في تحديث منصة Orion. تمت إضافة الكود الخبيث أثناء عملية البناء، لذا بدا مستودع الكود المصدري نظيفًا. كان نظام البناء القابل للتكرار سيجعل هذا قابلاً للاكتشاف — إعادة البناء من المصدر المنشور كانت ستنتج منتجًا مختلفًا عن الذي وُزع على العملاء.

XZ Utils (2024): استهدف هجوم متطور على سلسلة التوريد مكتبة الضغط xz. قام مشرف خبيث بإدخال كود باب خلفي مُعتَّم من خلال البنية التحتية للاختبار في نظام البناء. صُمم الكود المحقون لاختراق مصادقة SSH على أنظمة Linux المتأثرة. استغل الهجوم تعقيد عملية البناء، حيث حقن حمولة خبيثة من خلال ملفات اختبار ثنائية تتم معالجتها أثناء البناء. كانت عمليات البناء القابلة للتكرار والمراجعة الدقيقة لسلوك وقت البناء ستثير إشارات تحذيرية في وقت أبكر بكثير.

مصادر عدم القابلية للتكرار

فهم سبب اختلاف عمليات البناء بين التشغيلات هو الخطوة الأولى نحو إصلاحها. فيما يلي أكثر مصادر عدم الحتمية شيوعًا في عمليات البناء:

الطوابع الزمنية المضمنة في المنتجات

تقوم العديد من أدوات البناء بتضمين التاريخ والوقت الحالي في ملفات الإخراج. تحتوي ملفات Java JAR على طوابع زمنية في إدخالات ZIP الخاصة بها. قد يسجل مترجمو C/C++ وحدات الماكرو __DATE__ و __TIME__. تتضمن ملفات PE التنفيذية على Windows طابعًا زمنيًا في ترويساتها. في كل مرة تبني فيها، يتغير الطابع الزمني، مما ينتج مخرجات مختلفة.

ترتيب الملفات غير الحتمي

لا تضمن تنسيقات الأرشيف مثل tar و zip ترتيبًا ثابتًا للملفات. قد يعتمد الترتيب الذي تُضاف به الملفات إلى الأرشيف على ترتيب قائمة الدليل في نظام الملفات، والذي يمكن أن يختلف بين الأجهزة أو حتى بين التشغيلات على نفس الجهاز. ينتج عن هذا أرشيفات مختلفة بمحتويات متطابقة.

إصدارات التبعيات العائمة

إذا حدد تكوين البناء الخاص بك express: ^4.18.0 بدلاً من إصدار محدد، فقد تحصل على 4.18.1 اليوم و 4.18.2 غدًا. التبعيات غير المثبتة هي أحد أكثر مصادر عدم القابلية للتكرار شيوعًا وتأثيرًا.

اختلافات بيئة البناء

يمكن أن تؤثر إصدارات نظام التشغيل المختلفة، وإصدارات المترجم، وإصدارات مكتبات النظام، وإعدادات اللغة المحلية، وتكوينات المنطقة الزمنية، وحتى عدد أنوية المعالج على مخرجات البناء. قد يختلف البناء على Ubuntu 22.04 عنه على Ubuntu 24.04، حتى مع نفس المصدر والتبعيات.

التنزيلات من الشبكة أثناء البناء

عمليات البناء التي تقوم بتنزيل التبعيات في وقت البناء هي بطبيعتها غير قابلة للتكرار. قد يقدم سجل الحزم إصدارًا مختلفًا، وقد تعيد شبكة توصيل المحتوى (CDN) محتوى مخزنًا مؤقتًا أو محدثًا، أو قد تكون الشبكة غير متوفرة تمامًا. أي بناء يتطلب الوصول إلى الشبكة يكون تحت رحمة الأنظمة الخارجية.

القيم العشوائية وعناوين الذاكرة

تقوم بعض عمليات البناء بتضمين معرفات UUID عشوائية، أو تستخدم ترتيب تكرار غير حتمي لجداول التجزئة (hash maps)، أو تتضمن عناوين ذاكرة في مخرجاتها. يمكن أن تُدخل بيانات التنميط وتغطية الكود ورموز التصحيح جميعها عشوائية في منتجات البناء.

البناء المحكم: المعيار الذهبي

البناء المحكم (hermetic build) هو بناء مكتفٍ ذاتيًا بالكامل: لا يوجد وصول إلى الشبكة، وجميع المدخلات معلنة صراحة، وبيئة البناء محددة بالكامل. تعتبر عمليات البناء المحكمة المعيار الذهبي للقابلية للتكرار لأنها تقضي على فئات كاملة من عدم الحتمية بالتصميم.

ماذا يعني البناء المحكم

في البناء المحكم:

  • لا يمكن لعملية البناء الوصول إلى الشبكة. يجب جلب جميع التبعيات مسبقًا وإعلانها.
  • كل مدخل — الكود المصدري، التبعيات، سلسلة الأدوات، التكوين — مُدرج ومحدد الإصدار صراحة.
  • بيئة البناء محددة بدقة، بما في ذلك نظام التشغيل والحزم المثبتة ومتغيرات البيئة.
  • البناء معزول في صندوق رمل (sandbox) بحيث لا يمكنه قراءة ملفات غير معلنة من النظام المضيف.

Bazel كنظام بناء محكم

صُمم Bazel من الألف إلى الياء لعمليات البناء المحكمة والقابلة للتكرار. يعزل كل إجراء بناء في صندوق رمل، ويعلن جميع المدخلات والمخرجات صراحة، ويخزن النتائج مؤقتًا بناءً على تجزئة المدخلات (input hashes) بدلاً من الطوابع الزمنية. تحافظ ميزات التخزين المؤقت عن بُعد والتنفيذ عن بُعد في Bazel على الإحكام حتى في بيئات البناء الموزعة.

# Bazel WORKSPACE file: all external dependencies declared
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "com_google_protobuf",
    sha256 = "a79d19dcdf9139fa4b81206e318e33d245c4c9da1ffed21c87288f9142c5f4ef",
    strip_prefix = "protobuf-23.2",
    urls = ["https://github.com/protocolbuffers/protobuf/archive/v23.2.tar.gz"],
)

Docker Multi-Stage Builds مع صور أساسية مثبتة

يمكن أن تقترب Docker multi-stage builds من الإحكام عند دمجها مع صور أساسية مثبتة وتبعيات مُجلبة مسبقًا:

# Stage 1: Build with all dependencies pre-installed
FROM golang@sha256:2c3f3c4a1f8e4c2b7d5e1a9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -trimpath -ldflags='-s -w -buildid=' \
    -o /app/server ./cmd/server

# Stage 2: Minimal runtime image
FROM gcr.io/distroless/static@sha256:1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b AS runtime
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

لاحظ استخدام -trimpath لإزالة مسارات الملفات المحلية من الملف الثنائي، و -buildid= لمسح معرف البناء، و -s -w لتجريد معلومات التصحيح. هذه الأعلام ضرورية للقابلية للتكرار في بنيات Go.

ملفات القفل (Lock Files)

ملفات القفل هي الأداة الأكثر سهولة لتحسين القابلية للتكرار. تسجل الإصدارات الدقيقة المحلولة لجميع التبعيات، بما في ذلك التبعيات المتعدية:

  • Node.js: package-lock.json أو yarn.lock — استخدم دائمًا npm ci بدلاً من npm install
  • Go: go.sum — يسجل تجزئات تشفيرية لجميع إصدارات الوحدات
  • Python: poetry.lock أو مخرجات pip-compile — يثبت كل تبعية متعدية
  • Rust: Cargo.lock — يُلتزم به دائمًا للمشاريع الثنائية

يجب دائمًا إيداع ملفات القفل في نظام التحكم بالإصدارات. البناء الذي يتجاهل ملفات القفل هو بناء لا يمكنك تكراره.

تضمين التبعيات (Vendoring)

يذهب تضمين التبعيات أبعد من ملفات القفل عن طريق تخزين الكود المصدري الفعلي للتبعيات في مستودعك. هذا يزيل أي اعتماد على السجلات الخارجية في وقت البناء:

# Go: vendor all dependencies
go mod vendor

# Build using vendored dependencies
go build -mod=vendor ./cmd/server

يقايض التضمين حجم المستودع مقابل الموثوقية والقابلية للتكرار. وهو ذو قيمة خاصة لعمليات البناء التي يجب أن تعمل في بيئات معزولة عن الشبكة (air-gapped) أو للمشاريع التي تهم فيها القابلية للتكرار طويلة المدى.

مقايضات عملية: الإحكام مقابل تجربة المطور

الإحكام الكامل يأتي بتكلفة. Bazel لديه منحنى تعلم حاد. التضمين يزيد من حجم المستودع. الجلب المسبق لكل تبعية يتطلب بنية تحتية. المفتاح هو تبني الإحكام تدريجيًا: ابدأ بملفات القفل والصور المثبتة، ثم أضف التضمين والعزل حسب ما تتطلبه احتياجاتك الأمنية.

تثبيت كل شيء

التثبيت (pinning) هو ممارسة تحديد إصدارات دقيقة وغير قابلة للتغيير لكل مكون في بنائك. الوسوم (tags) قابلة للتغيير — يمكن نقلها للإشارة إلى محتوى مختلف. البصمات (digests) ومعرفات SHA للإيداعات غير قابلة للتغيير. ثبّت دائمًا على مراجع غير قابلة للتغيير.

تثبيت الصور الأساسية بالبصمة

وسوم صور Docker مثل node:20 أو python:3.12-slim يمكن أن تتغير في أي وقت. يمكن للسجل دفع صورة جديدة بنفس الوسم. ثبّت بالبصمة (digest) بدلاً من ذلك:

# BAD: tag can change at any time
FROM node:20-alpine

# GOOD: pinned to an immutable digest
FROM node@sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2

# BETTER: tag for readability, digest for immutability
FROM node:20-alpine@sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2

يمكنك إيجاد البصمة الحالية باستخدام:

docker inspect --format='{{index .RepoDigests 0}}' node:20-alpine

تثبيت GitHub Actions بمعرف SHA

وسوم GitHub Actions قابلة للتغيير. يمكن لإجراء مخترق دفع كود خبيث إلى وسم موجود. ثبّت دائمًا على معرف SHA الكامل للإيداع:

# BAD: tag can be moved to malicious commit
- uses: actions/checkout@v4

# GOOD: pinned to immutable commit SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

استخدم أدوات مثل ratchet أو pinact لأتمتة تثبيت SHA عبر ملفات سير العمل الخاصة بك والاحتفاظ بتعليقات مع وسم الإصدار لسهولة القراءة.

تثبيت إصدارات سلسلة الأدوات

سلسلة أدوات البناء — المترجم، بيئة التشغيل، مدير الحزم — يجب أن تكون مثبتة الإصدار. استخدم ملفات تكوين مدير الإصدارات لإعلان الإصدارات الدقيقة:

# .tool-versions (used by asdf version manager)
nodejs 20.11.0
python 3.12.1
golang 1.22.0
rust 1.75.0
# .nvmrc (Node version manager)
20.11.0
# rust-toolchain.toml
[toolchain]
channel = "1.75.0"
components = ["rustfmt", "clippy"]
targets = ["x86_64-unknown-linux-gnu"]

استخدام Nix لتكرار بيئة البناء

يوفر Nix النهج الأكثر صرامة لتكرار البيئة. يعلن Nix flake عن بيئة البناء الكاملة كدالة لمدخلاتها:

# flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system}; in {
        devShells.default = pkgs.mkShell {
          buildInputs = [ pkgs.go_1_22 pkgs.nodejs_20 pkgs.protobuf ];
        };
      });
}

مع Nix، يحصل كل مطور وكل مشغل CI على نفس الإصدارات الدقيقة لكل أداة، حتى مكتبات النظام. يثبت ملف flake.lock شجرة التبعيات بأكملها على مراجعات Git محددة.

التحقق من سلامة البناء

تحقيق القابلية للتكرار ليس سوى نصف المعركة. تحتاج أيضًا إلى التحقق منها — لتأكيد أن عمليات البناء المستقلة من نفس المصدر تنتج بالفعل منتجات متطابقة.

مقارنة عمليات البناء عبر البيئات

أبسط عملية تحقق هي بناء نفس المنتج في بيئتين مختلفتين ومقارنة المخرجات:

# Build in CI
sha256sum build/output/myapp.tar.gz
# Output: a1b2c3d4... build/output/myapp.tar.gz

# Rebuild locally from the same commit
git checkout v1.2.3
make build
sha256sum build/output/myapp.tar.gz
# Output should match: a1b2c3d4...

إذا تطابقت التجزئات، فإن البناء قابل للتكرار. إذا لم تتطابق، فأنت بحاجة إلى التحقيق في ما يختلف.

استخدام diffoscope للتحليل العميق

diffoscope هي أداة أساسية لتشخيص مشكلات القابلية للتكرار. تفك حزم الأرشيفات بشكل متكرر، وتفكك الملفات الثنائية، وتُظهر لك بالضبط أين يختلف بناءان:

# Install diffoscope
pip install diffoscope

# Compare two builds
diffoscope build-1/myapp.tar.gz build-2/myapp.tar.gz --html report.html

# Compare two container images
diffoscope image-1.tar image-2.tar --html report.html

يُظهر تقرير HTML الاختلافات على كل مستوى: بيانات الأرشيف الوصفية، محتويات الملفات، أقسام الملفات الثنائية، والموارد المضمنة. هذا لا يقدر بثمن لتحديد مصادر عدم الحتمية والقضاء عليها واحدة تلو الأخرى.

تخزين بيانات البناء الوصفية

حتى قبل تحقيق القابلية الكاملة للتكرار، يوفر تسجيل بيانات البناء الوصفية إمكانية التتبع. التقط وخزن:

  • معرف SHA لإيداع Git والفرع
  • تجزئات جميع تبعيات المدخلات
  • تفاصيل بيئة البناء (إصدار نظام التشغيل، إصدار سلسلة الأدوات، متغيرات البيئة)
  • تجزئات جميع منتجات المخرجات
  • طوابع البناء الزمنية ومدته

SLSA Provenance

SLSA provenance هو تنسيق موحد لبيانات البناء الوصفية. يسجل ما تم بناؤه، ومن أي مصدر، وباستخدام أي عملية بناء، وفي أي بيئة. يمكن لأدوات مثل slsa-github-generator توليد provenance موقعة تلقائيًا لبنيات GitHub Actions الخاصة بك:

# .github/workflows/release.yml
jobs:
  build:
    outputs:
      digest: ${{ steps.hash.outputs.digest }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - run: make build
      - id: hash
        run: echo "digest=$(sha256sum myapp | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"

  provenance:
    needs: build
    permissions:
      actions: read
      id-token: write
      contents: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
    with:
      base64-subjects: ${{ needs.build.outputs.digest }}

فحص صور الحاويات

بالنسبة لصور الحاويات، تحقق من السلامة عن طريق فحص طبقات الصورة بحثًا عن محتوى غير متوقع:

# List all layers in an image
docker history myapp:latest --no-trunc

# Export and inspect image contents
docker save myapp:latest -o image.tar
tar -tf image.tar

# Use dive to inspect layer contents interactively
dive myapp:latest

البناء القابل للتكرار في CI/CD

تُدخل منصات CI/CD تحديات القابلية للتكرار الخاصة بها. تتغير صور المشغلات، وتنتهي صلاحية ذاكرة التخزين المؤقت، وبيئات البناء زائلة. إليك كيفية تحقيق القابلية للتكرار على المنصات الرئيسية.

GitHub Actions

يثبت سير عمل GitHub Actions القابل للتكرار كل مكون خارجي:

name: Reproducible Build
on:
  push:
    branches: [main]

jobs:
  build:
    # Pin the runner image (or use a self-hosted runner with a known image)
    runs-on: ubuntu-22.04

    steps:
      # Pin action by SHA
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

      # Pin setup action and toolchain version
      - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
        with:
          go-version: '1.22.0' # Exact version, not '1.22.x'

      # Cache with hash-based key for determinism
      - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1
        with:
          path: ~/go/pkg/mod
          key: go-mod-${{ hashFiles('go.sum') }}

      # Build with reproducibility flags
      - run: |
          CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
          go build -trimpath -ldflags='-s -w -buildid=' \
          -o myapp ./cmd/server

      # Record artifact hash
      - run: sha256sum myapp >> checksums.txt

      - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
        with:
          name: build-artifacts
          path: |
            myapp
            checksums.txt

GitLab CI

في GitLab CI، استخدم صور Docker ثابتة للمشغلات وثبّت جميع التبعيات:

# .gitlab-ci.yml
variables:
  # Use a specific image digest for the build environment
  BUILD_IMAGE: golang@sha256:2c3f3c4a1f8e4c2b7d5e1a9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b

stages:
  - build
  - verify

build:
  stage: build
  image: $BUILD_IMAGE
  script:
    - go mod download
    - CGO_ENABLED=0 GOOS=linux GOARCH=amd64
      go build -trimpath -ldflags='-s -w -buildid='
      -o myapp ./cmd/server
    - sha256sum myapp | tee checksums.txt
  artifacts:
    paths:
      - myapp
      - checksums.txt
  cache:
    key:
      files:
        - go.sum
    paths:
      - /go/pkg/mod

verify-reproducibility:
  stage: verify
  image: $BUILD_IMAGE
  script:
    - go mod download
    - CGO_ENABLED=0 GOOS=linux GOARCH=amd64
      go build -trimpath -ldflags='-s -w -buildid='
      -o myapp-verify ./cmd/server
    - sha256sum myapp-verify
    - diff <(sha256sum myapp | cut -d' ' -f1) <(sha256sum myapp-verify | cut -d' ' -f1)

استخدام Nix في CI للقابلية الكاملة للتكرار

يوفر Nix أقوى ضمانات القابلية للتكرار في CI من خلال تحديد إغلاق البناء بالكامل (build closure):

# GitHub Actions with Nix
name: Nix Build
on: push

jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - uses: cachix/install-nix-action@ba0dd844c9180cbf77aa557a09b7b0d890fbd0fb # v26
        with:
          nix_path: nixpkgs=channel:nixos-24.05
      - run: nix build .#myapp
      - run: sha256sum result/bin/myapp

مع Nix، يثبت ملف flake.lock الإصدار الدقيق لكل حزمة في إغلاق البناء. سيحصل مطوران يشغلان nix build على أجهزة مختلفة على مخرجات متطابقة، لأن رسم التبعيات بالكامل — بما في ذلك مترجم C ومكتبات النظام وكل تبعية متعدية — محدد بدقة.

بناء صور حاويات قابلة للتكرار

يتطلب بناء صور حاويات قابلة للتكرار عناية خاصة. يضمن docker build التقليدي طوابع زمنية في كل طبقة. توفر أدوات مثل kaniko و BuildKit و ko قابلية تكرار أفضل:

# Using BuildKit with reproducible output
DOCKER_BUILDKIT=1 docker build \
  --build-arg SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) \
  --output type=docker,rewrite-timestamp=true \
  -t myapp:latest .

متغير البيئة SOURCE_DATE_EPOCH هو آلية موحدة لإخبار أدوات البناء باستخدام طابع زمني ثابت بدلاً من الوقت الحالي. تحترمه العديد من الأدوات، بما في ذلك GCC و dpkg و tar و zip.

عندما لا تكون القابلية المثالية للتكرار ممكنة

القابلية للتكرار بت-لكل-بت هي المثال الأعلى، لكنها ليست قابلة للتحقيق دائمًا — وهذا مقبول. المهم هو فهم موقعك على طيف القابلية للتكرار وتطبيق ضوابط تعويضية حيث لزم الأمر.

عدم القابلية للتكرار المقبول

بعض عدم القابلية للتكرار غير ضار. طابع زمني للبناء في ملف بيانات وصفية لا يُنفَّذ أبدًا لا يؤثر على أمان المنتج. إدخال سجل يسجل متى تم تشغيل البناء ليس خطرًا أمنيًا. التمييز الرئيسي هو ما إذا كان العنصر غير القابل للتكرار في المنتج نفسه (خطر) أو فقط في البيانات الوصفية المرافقة له (مقبول).

القابلية للتكرار "الكافية"

بالنسبة للعديد من الفرق، الهدف العملي هو القابلية الوظيفية للتكرار: نفس المدخلات تنتج نفس المخرجات الوظيفية. قد تختلف الملفات الثنائية في الطوابع الزمنية المضمنة أو رموز التصحيح، لكنها تتصرف بشكل متطابق. هذا المستوى من القابلية للتكرار قابل للتحقيق بالأدوات والممارسات القياسية:

  • ثبّت جميع إصدارات التبعيات باستخدام ملفات القفل
  • ثبّت الصور الأساسية بالبصمة
  • ثبّت إجراءات CI بمعرف SHA
  • استخدم إصدارات ثابتة لسلسلة الأدوات
  • أزل الطوابع الزمنية حيثما أمكن

الضوابط التعويضية

عندما لا تكون القابلية الكاملة للتكرار ممكنة، توفر الضوابط التعويضية ضمانات بديلة:

  • توقيع الكود (code signing): وقّع منتجاتك تشفيريًا حتى يتمكن المستهلكون من التحقق من أنها جاءت من نظام البناء الخاص بك.
  • SLSA provenance: أنشئ وانشر بيانات provenance الوصفية التي تسجل مدخلات البناء والبيئة والعملية.
  • قائمة مواد البرمجيات (SBOM): انشر قائمة كاملة بالمكونات في منتجك حتى يعرف المستهلكون بالضبط ما يحصلون عليه.
  • الاحتفاظ بسجلات البناء: خزن سجلات البناء الكاملة للتحليل الجنائي إذا اشتُبه في اختراق.
  • عمليات البناء متعددة الأطراف: اطلب من عدة أطراف مستقلة البناء من نفس المصدر ومقارنة النتائج.

طيف القابلية للتكرار

فكر في القابلية للتكرار كطيف، وليس حالة ثنائية:

  • المستوى 0 — لا شيء: لا تثبيت للإصدارات، لا ملفات قفل، عمليات البناء تعتمد على أحدث ما هو متاح. هذا هو المكان الذي تبدأ منه معظم المشاريع.
  • المستوى 1 — تبعيات مثبتة: ملفات القفل مودعة، التبعيات بإصدارات ثابتة. عمليات البناء قابلة للتكرار في الغالب.
  • المستوى 2 — بيئة مثبتة: إصدارات سلسلة الأدوات والصور الأساسية مثبتة. بيئة البناء مضبوطة.
  • المستوى 3 — بناء محكم: لا وصول إلى الشبكة أثناء البناء. جميع المدخلات معلنة صراحة. ضمانات قوية للقابلية للتكرار.
  • المستوى 4 — قابل للتكرار بت-لكل-بت: عمليات البناء المستقلة تنتج منتجات متطابقة. التحقق الكامل ممكن.

كل مستوى يبني على سابقه. الانتقال من المستوى 0 إلى المستوى 1 غالبًا ما يكون التحسين الأعلى تأثيرًا الذي يمكنك إجراؤه، ويتطلب جهدًا ضئيلاً.

الخاتمة

عمليات البناء القابلة للتكرار هي أساس الثقة في سلسلة التوريد. بدونها، أنت تعتمد على إيمان أعمى بأن نظام البناء الخاص بك لم يتعرض للاختراق — وهو إيمان تعلم عملاء SolarWinds ومستخدمو XZ Utils وعدد لا يحصى من الآخرين أنه في غير محله.

الخبر الجيد هو أنك لست بحاجة إلى تحقيق الكمال من اليوم الأول. ابدأ بالأساسيات:

  • أودع ملفات القفل الخاصة بك واستخدم npm ci بدلاً من npm install.
  • ثبّت صورك الأساسية بالبصمة في كل Dockerfile.
  • ثبّت إجراءات CI الخاصة بك بمعرف SHA في كل ملف سير عمل.
  • ثبّت إصدارات سلسلة الأدوات باستخدام .tool-versions أو rust-toolchain.toml أو ما شابه.

ثم أضف الإحكام تدريجيًا: ضمّن تبعياتك، واستخدم Nix أو Bazel لعزل البناء، وأزل الطوابع الزمنية من منتجاتك، وأعد وظائف تحقق تعيد البناء وتقارن.

كل خطوة على طيف القابلية للتكرار تجعل عمليات البناء أكثر جدارة بالثقة، وسلسلة التوريد أكثر قابلية للتدقيق، وبرمجياتك أكثر أمانًا. في عالم تُعد فيه أنظمة البناء ناقل هجوم أساسي، فإن عمليات البناء القابلة للتكرار ليست اختيارية — إنها ضرورية.