إدارة الأسرار والبنية التحتية للمفاتيح

الأسرار في Kubernetes وCI

18 دقيقة الدرس 6 من 28

الأسرار في Kubernetes وCI

أسرار Kubernetes الأصلية مُشفَّرة بـ base64 فقط، وليست مُعمَّاة. أي مهندس لديه صلاحية kubectl get secret يمكنه قراءة كل كلمة مرور ومفتاح API وشهادة TLS في الـ namespace. في Google وMeta وNetflix، تُعامَل منظومة أسرار Kubernetes كـ ناقل توزيع لا كخزنة — مصدر الحقيقة الفعلي يقع في HashiCorp Vault أو AWS Secrets Manager أو GCP Secret Manager. الأداتان اللتان تجسّران هذه الهوة هما External Secrets Operator (ESO) وSecrets Store CSI Driver. لخطوط CI، الإجابة الحديثة على مشكلة بيانات الاعتماد هي مصادقة OIDC بلا مفاتيح. يغطي هذا الدرس الثلاثة، إضافة إلى أنماط الفشل التي تُسرِّب الأسرار إلى سجلات البناء وتفريغ البيئة.

External Secrets Operator (ESO)

يعمل ESO كمتحكم داخل المجموعة. تُنشئ CRD باسم ExternalSecret يُشير إلى إدخال في مخزن الأسرار الخارجي؛ يجلبه ESO بفاصل زمني قابل للضبط ويُجسّده كـ Secret Kubernetes اعتيادي. التطبيقات تقرأ الـ Secret الأصلي — لا تحتاج أي SDK أو sidecar. يبقى المخزن الخارجي مصدر الحقيقة الوحيد؛ ESO هو محرك مزامنة للقراءة فقط.

النموذج ثنائي الطبقات: SecretStore (أو ClusterSecretStore) يحمل إعدادات المزود وبيانات اعتماده، بينما يُعلن ExternalSecret أي مفاتيح تُزامَن. الفصل بينهما يعني أن فريق المنصة يُدير بيانات اعتماد المخزن بينما تُؤلّف فرق التطبيقات كائنات ExternalSecret خاصة بها.

# تثبيت ESO عبر Helm helm repo add external-secrets https://charts.external-secrets.io helm install external-secrets external-secrets/external-secrets \ -n external-secrets --create-namespace \ --set installCRDs=true --- # ClusterSecretStore: فريق المنصة يمتلك هذا (واحد لكل منطقة مجموعة) apiVersion: external-secrets.io/v1beta1 kind: ClusterSecretStore metadata: name: aws-secrets-manager spec: provider: aws: service: SecretsManager region: us-east-1 auth: jwt: serviceAccountRef: name: external-secrets-sa namespace: external-secrets --- # ExternalSecret: فريق التطبيق يُؤلّف هذا لكل خدمة apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: payments-db-creds namespace: payments spec: refreshInterval: 1h # أعد المزامنة كل ساعة؛ الدوران بدون إعادة تشغيل الـ pod secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: payments-db-secret # الـ K8s Secret الذي سينشئه/يُحدّثه ESO creationPolicy: Owner # ESO يمتلك الـ Secret؛ حذف ExternalSecret يحذف Secret template: engineVersion: v2 data: DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@pg.internal:5432/payments" data: - secretKey: username remoteRef: key: prod/payments/db property: username - secretKey: password remoteRef: key: prod/payments/db property: password
لماذا creationPolicy: Owner مهم: عند حذف ExternalSecret، يُزيل ESO تلقائياً الـ Kubernetes Secret المُشتَق. بدون ذلك، تبقى الأسرار اليتيمة في الـ namespace إلى أجل غير مسمى، متراكمةً ببيانات اعتماد قديمة تُفشل عمليات التدقيق وتُربك المشغّلين. اضبط دائماً Owner في الإنتاج؛ استخدم Merge فقط حين تجمع عمداً عدة ExternalSecrets في Secret واحد.

Secrets Store CSI Driver

بينما يُجسّد ESO Secret Kubernetes (الذي يستقر في etcd)، يأخذ CSI Driver نهجاً مختلفاً: يُثبّت السر مباشرةً في نظام ملفات الـ pod كملف، متجاوزاً etcd كلياً. لا يلمس السر مستوى التحكم في Kubernetes أثناء السكون. يُلبّي هذا متطلبات الامتثال الأكثر صرامة (PCI DSS Level 1، FedRAMP High) حيث حتى تخزين etcd المُعمَّى غير مقبول.

يتكون Driver من DaemonSet على كل عقدة وإضافات خاصة بالمزود. يستخدم مزود AWS الـ IRSA (IAM Roles for Service Accounts)؛ مزود Azure يستخدم Managed Identity؛ مزود Vault يستخدم نمط vault agent injector لكن بدون sidecar.

# تثبيت CSI driver + مزود AWS helm repo add secrets-store-csi-driver \ https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts helm install csi-secrets-store \ secrets-store-csi-driver/secrets-store-csi-driver \ -n kube-system \ --set syncSecret.enabled=true \ --set enableSecretRotation=true \ --set rotationPollInterval=3600s helm repo add aws-secrets-manager \ https://aws.github.io/secrets-store-csi-driver-provider-aws helm install -n kube-system aws-provider \ aws-secrets-manager/secrets-store-csi-driver-provider-aws --- # SecretProviderClass: يُعيّن AWS secret إلى مسار التثبيت apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: payments-db-spc namespace: payments spec: provider: aws parameters: objects: | - objectName: "prod/payments/db" objectType: "secretsmanager" jmesPath: - path: username objectAlias: db_username - path: password objectAlias: db_password secretObjects: # اختياري: مزامنة إلى K8s Secret أيضاً - secretName: payments-db-csi type: Opaque data: - objectName: db_username key: username - objectName: db_password key: password --- # Pod يستخدم تثبيت CSI apiVersion: v1 kind: Pod metadata: name: payments-api namespace: payments spec: serviceAccountName: payments-sa # يجب أن يحمل annotation الـ IRSA containers: - name: api image: payments-api:v2.3.1 volumeMounts: - name: secrets-vol mountPath: "/mnt/secrets" readOnly: true env: - name: DB_USERNAME valueFrom: secretKeyRef: name: payments-db-csi key: username volumes: - name: secrets-vol csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: payments-db-spc
ESO vs CSI Driver: Two Secret Delivery Paths AWS Secrets Manager / Vault ESO Controller ExternalSecret CRD etcd K8s Secret (enc) Pod A env / volume from Secret CSI DaemonSet SecretProviderClass Pod B tmpfs mount /mnt/secrets/* fetch write Secret mount fetch direct tmpfs (no etcd) Path 1: ESO (عبر etcd) Path 2: CSI Driver (تجاوز etcd)
ESO يكتب الأسرار في etcd كـ Kubernetes Secrets أصلية؛ CSI Driver يتجاوز etcd كلياً، مُثبّتاً الأسرار مباشرةً في ذاكرة الـ pod عبر tmpfs.

مصادقة OIDC بلا مفاتيح في CI

مشكلة أسرار CI التقليدية: خط الأنابيب يحتاج رمزاً لـ AWS أو Vault، لذا تُخزّنه في متغيرات GitHub/GitLab CI — الآن بيانات الاعتماد الثابتة هذه سر طويل الأمد يمكن سرقته من مخرجات السجل أو تفريغ البيئة أو runner مُخترَق. تُزيل مصادقة OIDC بلا مفاتيح بيانات الاعتماد الثابتة من خطوط CI كلياً.

الآلية: حين تعمل وظيفة GitHub Actions، يُصدر مزود OIDC الخاص بـ GitHub JWT قصير الأمد (مُحدَّد النطاق، موقَّع من GitHub) يُثبت أي مستودع وفرع وسير عمل شغَّل الوظيفة. يُهيَّأ AWS (أو Vault أو GCP أو Azure) للوثوق بمزود OIDC الخاص بـ GitHub ومبادلة الـ JWT ببيانات اعتماد سحابية صالحة لمدة الوظيفة — عادةً 15 دقيقة.

# الخطوة 1: إنشاء ثقة OIDC في AWS (Terraform) resource "aws_iam_openid_connect_provider" "github" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] } resource "aws_iam_role" "github_deploy" { name = "github-deploy-payments" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Federated = aws_iam_openid_connect_provider.github.arn } Action = "sts:AssumeRoleWithWebIdentity" Condition = { StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" # تقييد مستودع محدد وفرع main فقط — حرج "token.actions.githubusercontent.com:sub" = "repo:my-org/payments:ref:refs/heads/main" } } }] }) } --- # الخطوة 2: سير عمل GitHub Actions — بدون أسرار إطلاقاً name: Deploy on: push: branches: [main] permissions: id-token: write # مطلوب: يسمح للـ runner بطلب رمز OIDC contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Configure AWS via OIDC uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/github-deploy-payments aws-region: us-east-1 # لا access-key-id ولا secret-access-key مطلوبان - name: Push image to ECR run: | aws ecr get-login-password | docker login --username AWS \ --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com docker build -t payments-api . docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/payments-api:$GITHUB_SHA - name: Run database migration via SSM (لا تُسجّل بيانات الاعتماد أبداً) run: | aws ssm send-command \ --document-name "AWS-RunShellScript" \ --targets "Key=tag:Role,Values=payments-db" \ --parameters 'commands=["cd /app && php artisan migrate --force"]' \ --output-s3-bucket-name my-ssm-logs
ممارسة احترافية — تحديد نطاق sub claim: المطالبة sub في سياسة ثقة IAM هي التحكم الأساسي في نطاق التأثير. repo:my-org/payments:ref:refs/heads/main تُقيّد الدور على دفعات فرع main فقط. طلب سحب من fork لا يستطيع افتراض هذا الدور. في المؤسسات الكبيرة، أنشئ دوراً منفصلاً لكل بيئة (staging مقابل production) مع شروط sub مختلفة — لا تسمح أبداً لـ pipeline التجهيز بافتراض دور نشر الإنتاج.

تجنب تسريب متغيرات البيئة في CI وKubernetes

حتى حين تُسلَّم الأسرار بشكل صحيح، تتسرب عادةً عبر أخطاء تشغيلية. هذه هي أنماط الفشل الموثّقة في postmortems الحوادث الحقيقية:

  • حقن السجل عبر set -x: وضع تصحيح Bash يطبع كل أمر مع وسيطاته المُوسَّعة. سكريبت يُشغّل curl -H "Authorization: Bearer $TOKEN" ... مع تفعيل set -x سيطبع الرمز حرفياً. لا تستخدم set -x في سكريبتات CI التي تتعامل مع الأسرار؛ استخدم set -e (الفشل السريع) بدلاً منه.
  • تفريغ البيئة: env وprintenv وkubectl exec -- env وصفحات تصحيح الأطر (Django DEBUG=True، Laravel APP_DEBUG=true) ستطبع كل متغير بيئة. عطّل نقاط النهاية التصحيحية في الإنتاج ولا تُشغّل env في خطوة CI السجل.
  • Docker build-time ARGs: docker build --build-arg DB_PASSWORD=secret يُضمّن القيمة في تاريخ طبقات الصورة، قابلة للقراءة من قِبَل أي شخص لديه docker history myimage. استخدم multi-stage builds ومرّر الأسرار وقت التشغيل؛ لاحتياجات وقت البناء، استخدم Docker BuildKit secret mounts.
  • Kubernetes Secret في YAML مُودَع في Git: سر مُشفَّر بـ base64 في YAML مُودَع فعلياً نص عادي. استخدم SOPS أو Sealed Secrets للتعمية قبل الإيداع، أو الأفضل، لا تُودع قيم الأسرار إطلاقاً — خزّنها في المخزن الخارجي وأشر إليها عبر ESO.
  • مخرجات describe وحصص الموارد: kubectl describe pod يُظهر إدخالات env بما فيها القيم من valueFrom.secretKeyRef — تلك القيم تظهر مُخفَّاة في إصدارات Kubernetes الحديثة، لكن المجموعات الأقدم تكشفها. دقّق من لديه صلاحية RBAC لـ describe على الـ namespace.
# نمط تمرير الأسرار الآمن في CI (GitHub Actions) - name: Build with BuildKit secret (لا يدخل طبقات الصورة أبداً) run: | docker buildx build \ --secret id=npmrc,src=$HOME/.npmrc \ --tag my-app:$GITHUB_SHA \ . # في Dockerfile: # RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ # npm install # إخفاء قيم إضافية تظهر في السجلات - name: Fetch and mask deploy token run: | DEPLOY_TOKEN=$(aws secretsmanager get-secret-value \ --secret-id prod/deploy-token \ --query SecretString --output text) echo "::add-mask::$DEPLOY_TOKEN" # GitHub يُخفي هذه القيمة في كل أسطر السجل اللاحقة echo "DEPLOY_TOKEN=$DEPLOY_TOKEN" >> $GITHUB_ENV
مصيدة إنتاجية — كشف GITHUB_ENV: كتابة سر إلى $GITHUB_ENV يجعله متاحاً كمتغير بيئة للخطوات اللاحقة، لكنه يظهر أيضاً في مخرجات Set up job في بعض إصدارات runner. للأسرار التي يجب تمريرها بين الخطوات، يُفضَّل كتابتها إلى ملف مؤقت بصلاحيات 600، أو استخدام متغيرات إخراج GitHub Actions مع الإخفاء. لا تكتب أبداً قيمة سر خام إلى خطوة سجل أو رفع artifact.

ESO مقابل CSI Driver: متى تستخدم أيهما

كلا الأداتين تحلان المشكلة ذاتها لكنهما تناسبان متطلبات امتثال مختلفة. ESO هو الافتراضي الصحيح لغالبية أحمال العمل: أبسط تشغيلاً، يتكامل مع أي أداة Kubernetes تقرأ Secrets الأصلية، ويدعم الدوران بدون إعادة تشغيل الـ pods. استخدم CSI Driver حين يُلزم الامتثال بأن الأسرار لا يجب أن تلمس etcd إطلاقاً — بيئات PCI DSS، أحمال العمل الحكومية، أو السيناريوهات التي لا تتحكم فيها بالتعمية في المستوى التحكمي.

في الممارسة، تُشغّل المنصات الكبيرة كليهما: ESO لأسرار التطبيقات (بيانات اعتماد قواعد البيانات، مفاتيح API) وCSI Driver لمفاتيح TLS الخاصة والمواد المدعومة بـ HSM حيث متطلب الحدود التشفيرية أكثر صرامة.

الخلاصة

أسرار Kubernetes الأصلية هي آلية توزيع لا حدود ثقة. External Secrets Operator يُزامن الأسرار من Vault أو مخازن الأسرار السحابية إلى Secrets الأصلية عبر نموذج سحب، محتفظاً بالمخزن الخارجي كمصدر للحقيقة. CSI Driver يُسلّم الأسرار كتثبيتات tmpfs، متجاوزاً etcd للبيئات عالية الامتثال. مصادقة OIDC بلا مفاتيح تُزيل بيانات الاعتماد الثابتة من خطوط CI كلياً، مستعيضةً عنها برموز قصيرة الأمد ومُحدَّدة النطاق مرتبطة بمستودع وفرع محددين. منع تسريب متغيرات البيئة هو انضباط تشغيلي — الأدوات ضرورية لكن غير كافية بدون نظافة السجلات وانضباط طبقات Docker وتحديد نطاق RBAC على صلاحية describe.