امشب اطلاعات ورود آزمایشی را در یک Secret می‌گذاریم و به‌شکل فایل در اختیار برنامه قرار می‌دهیم. در پایان باید بتوانید وجود فایل، مجوز خواندن و رفتار تعویض رمز را بررسی کنید، بدون آن‌که مقدار رمز در گزارش یا مخزن Git قرار بگیرد.

محیط و مرز تمرین#

یک کلاستر آزمایشگاهی با گره‌های Linux، ابزار kubectl و دسترسی ساخت namespace لازم است. پیش از اجرا با kubectl config current-context مقصد را بررسی کنید؛ نام فضای این تمرین course-night-9 است و در پایان حذف می‌شود. در Bash کار کنید و از نام کاربری و رمز ساختگی استفاده کنید. هیچ سرویس واقعی در این درس رمز را تأیید نمی‌کند؛ برنامه فقط خوانا و غیرخالی بودن فایل‌ها را می‌سنجد.

فایل تعریف منابع یا manifest را می‌توان برای بازبینی نگه داشت، چون تنها نام Secret را دارد. خروجی YAML خود Secret چنین وضعیتی ندارد: مقدار کدگذاری‌شده هم باید مثل رمز محافظت شود. پیش از ورود تعاملی، ثبت فرمان‌ها با set -x را خاموش کنید و از ترمینال ضبط‌شونده برای اطلاعات واقعی استفاده نکنید.

Secret چه کاری می‌کند و چه کاری نمی‌کند؟#

Secret یک منبع در API کوبرنتیز برای نگه‌داری دادهٔ حساس است. مقدارها در نمایش YAML با Base64، یک کدگذاری برگشت‌پذیر، نمایش داده می‌شوند. این کار رمزنگاری نیست. محرمانگی به کنترل دسترسی مبتنی بر نقش یا RBAC، رمزنگاری پایگاه دادهٔ کلاستر یعنی etcd، انتقال امن، امنیت گره و جلوگیری از ثبت رمز در لاگ وابسته است.

هرکس بتواند یک Secret را از API با get بخواند، معمولاً می‌تواند مقدار آن را بازیابی کند. کسی که اجازهٔ ساخت Pod و Mount کردن آن Secret را دارد نیز ممکن است بتواند داده را بخواند. اصل حداقل دسترسی باید هر دو مسیر را پوشش دهد.

Manifest خوانندهٔ Secret#

فایل app.yaml فقط وجود فایل‌های Credential را بررسی می‌کند و مقدارشان را نمایش نمی‌دهد:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: secret-reader
  namespace: course-night-9
spec:
  replicas: 1
  selector:
    matchLabels:
      app: secret-reader
  template:
    metadata:
      labels:
        app: secret-reader
    spec:
      automountServiceAccountToken: false
      containers:
        - name: reader
          image: busybox:1.36
          command: ["/bin/sh", "-ec"]
          args:
            - |
              test -s /var/run/course/username
              test -s /var/run/course/password
              echo "Secret files exist; values were not printed"
              exec sleep 3600
          volumeMounts:
            - name: credentials
              mountPath: /var/run/course
              readOnly: true
      volumes:
        - name: credentials
          secret:
            secretName: app-credentials
            defaultMode: 0400

ساخت بدون قرار دادن مقدارها در تاریخچهٔ Shell#

ابتدا یک دایرکتوری موقت محلی بسازید و مقدارها را تعاملی دریافت کنید:

kubectl create namespace course-night-9
set +x
tmp_dir="$(mktemp -d)"
read -r -p 'Username: ' secret_user
read -r -s -p 'Password: ' secret_password; printf '\n'
printf '%s' "$secret_user" > "$tmp_dir/username"
printf '%s' "$secret_password" > "$tmp_dir/password"
unset secret_user secret_password

kubectl create secret generic app-credentials \
  --namespace course-night-9 \
  --from-file=username="$tmp_dir/username" \
  --from-file=password="$tmp_dir/password" \
  --dry-run=client -o yaml |
kubectl apply --server-side --field-manager=course-secret -f -

پس از ساخت Secret، فایل‌های موقت را حذف کنید:

rm -f "$tmp_dir/username" "$tmp_dir/password"
rmdir "$tmp_dir"
unset tmp_dir

برای Credential واقعی از --from-literal=password=... استفاده نکنید؛ مقدار ممکن است در تاریخچهٔ Shell و فهرست Processها افشا شود.

Mount و بررسی امن#

kubectl apply -f app.yaml
kubectl rollout status deployment/secret-reader --namespace course-night-9
kubectl logs deployment/secret-reader --namespace course-night-9
kubectl exec --namespace course-night-9 deployment/secret-reader -- \
  sh -ec 'stat -c "%a %n" /var/run/course/*; test -s /var/run/course/username; test -s /var/run/course/password'

این بررسی‌ها بدون نمایش محتوا ثابت می‌کنند که فایل‌ها وجود دارند و خالی نیستند. Volume فقط خواندنی است و Permission فایل 0400 درخواست شده است.

گزینهٔ -e در فرمان آغاز کانتینر باعث می‌شود شکست هر بررسی، اجرای برنامه را متوقف کند. بدون آن، ممکن است test شکست بخورد اما پیام موفقیت و سپس sleep اجرا شود؛ در این حالت لاگ شاهد معتبری برای موفقیت نیست. در فرمان تعاملی بررسی نیز از sh -ec استفاده کنید تا خطای مرحلهٔ اول پشت موفقیت مرحلهٔ آخر پنهان نشود.

این نمونهٔ ساده با کاربر پیش‌فرض root ایمیج اجرا می‌شود؛ فایل 0400 فقط برای مالک خواندنی است. اگر سیاست کلاستر اجرای بدون root را الزام می‌کند، باید مالکیت گروهی volume، مقدار fsGroup و حالت فایل را با هویت برنامه هماهنگ کنید. تغییر مجوز به خواندن برای همه، راه‌حل مناسبی برای رفع خطای برنامه نیست. غیرفعال‌کردن automountServiceAccountToken نیز فقط توکن خودکار حساب سرویس را حذف می‌کند؛ مانع خواندن Secret صریحاً نصب‌شده در همین کانتینر نمی‌شود.

Metadata را بررسی کنید، نه داده را:

kubectl get secret app-credentials --namespace course-night-9
kubectl describe secret app-credentials --namespace course-night-9
kubectl auth can-i get secrets --namespace course-night-9

describe نام کلیدها و تعداد Byteها را بدون Decode کردن مقدارها گزارش می‌کند.

پاسخ can-i دربارهٔ هویت فعلی kubectl است، نه همهٔ کاربران یا حساب برنامه. برای بررسی سیاست تیم، باید هویت و نقش هر مصرف‌کننده جدا ارزیابی شود. در گزارش این تمرین فقط نام فضا، وضعیت Pod و نتیجهٔ آزمون را نگه دارید؛ حتی نام کلیدها ممکن است برای گزارش عمومی لازم نباشد.

تعویض اطلاعات ورود#

با همان الگوی ورودی امن، فایل‌های موقت تازه‌ای بسازید:

tmp_dir="$(mktemp -d)"
read -r -p 'Username: ' secret_user
read -r -s -p 'New password: ' secret_password; printf '\n'
printf '%s' "$secret_user" > "$tmp_dir/username"
printf '%s' "$secret_password" > "$tmp_dir/password"
unset secret_user secret_password

kubectl create secret generic app-credentials \
  --namespace course-night-9 \
  --from-file=username="$tmp_dir/username" \
  --from-file=password="$tmp_dir/password" \
  --dry-run=client -o yaml |
kubectl apply --server-side --field-manager=course-secret -f -

این خط لوله YAML را مستقیم به API می‌دهد؛ آن را با tee یا خروجی اشکال‌زدایی ثبت نکنید. اعمال سمت سرور از ایجاد annotation نسخهٔ قبلی توسط apply سمت کلاینت پرهیز می‌کند، اما داده همچنان در API ذخیره می‌شود. اگر تعارض مالکیت فیلد داشتید، مدیر قبلی منبع را بررسی کنید و با اجبار، تغییر ابزار دیگری را نپوشانید.

فایل‌های Secret پس از مدتی به‌روز می‌شوند، اما برنامه ممکن است مقدار قبلی را در حافظه نگه دارد. نصب با subPath این به‌روزرسانی خودکار را دریافت نمی‌کند؛ مقدار محیطی نیز در فرایند موجود عوض نمی‌شود. در این آزمایش، راه‌اندازی مجدد کنترل‌شده روش ساده‌ای برای خواندن نسخهٔ تازه است:

kubectl rollout restart deployment/secret-reader --namespace course-night-9
kubectl rollout status deployment/secret-reader --namespace course-night-9
kubectl logs deployment/secret-reader --namespace course-night-9

فایل‌های موقت را بلافاصله پاک کنید:

rm -f "$tmp_dir/username" "$tmp_dir/password"
rmdir "$tmp_dir"
unset tmp_dir

در یک برنامهٔ واقعی، تعویض Secret به‌تنهایی رمز پایگاه داده را تغییر نمی‌دهد. باید ابتدا اعتبار تازه در سرویس مقصد پذیرفته شود، سپس مصرف‌کننده‌ها جابه‌جا شوند و در پایان اعتبار قدیمی لغو شود. آزمون واقعی شامل ورود موفق با مقدار تازه و ردشدن مقدار قدیمی است. این مصرف‌کنندهٔ آزمایشی چنین اتصال خارجی‌ای ندارد؛ مشاهدهٔ پیام آن را با تکمیل چرخهٔ تعویض رمز اشتباه نگیرید.

سرویس مدیریت راز می‌تواند صدور و همگام‌سازی را انجام دهد. Git باید فقط ارجاع یا شکل رمزنگاری‌شدهٔ مناسب فرایند تیم را نگه دارد؛ کلید رمزگشایی هم جزو مخزن نیست.

تمرین خرابی: نام اشتباه Secret#

موقتاً secretName را در app.yaml به missing-credentials تغییر دهید:

kubectl apply -f app.yaml
kubectl get pods --namespace course-night-9
kubectl describe pod <new-pod-name> --namespace course-night-9

Pod در مرحلهٔ ساخت Container منتظر می‌ماند، زیرا Volume اجباری قابل Mount نیست. نام را تعمیر و دوباره Apply کنید.

Pod قدیمی ممکن است همچنان سالم بماند، چون Deployment هنگام جایگزینی تدریجی آن را نگه داشته است. نام Pod جدید و پیام رویداد مربوط به volume را بررسی کنید؛ وجود یک Pod سالم، خرابی نسخهٔ تازه را رد نمی‌کند. اگر فایل رمز را عمداً خالی بسازید، خطای برنامه پس از شروع کانتینر رخ می‌دهد و با Secret گم‌شده که مانع نصب volume می‌شود فرق دارد. این دو خرابی را جدا ثبت و سپس مقدار سالم را برگردانید.

چک‌لیست پایان#

  • Credentialها را بدون قرار دادن مقدار در Git یا Argument فرمان ساخته‌ام.
  • Mount را بدون چاپ مقدارها بررسی کرده‌ام.
  • توضیح می‌دهم چرا Base64 رمزنگاری نیست.
  • خرابی Volume ناشی از Secret گم‌شده را تعمیر کرده‌ام.
  • مستند کرده‌ام چه کسانی باید اجازهٔ خواندن یا Mount کردن این Secret را داشته باشند.
kubectl delete namespace course-night-9

حذف فایل موقت، تضمین پاک‌شدن فیزیکی همهٔ نسخه‌ها از دیسک یا پشتیبان دستگاه نیست. در صورت افشای اعتبار واقعی باید آن را لغو کرد؛ پاک‌کردن فایل یا تاریخچه کافی نیست.

منابع#