ABC للأمان في Kubernetes: المصادقة ، التفويض ، التدقيق

ABC للأمان في Kubernetes: المصادقة ، التفويض ، التدقيق

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

المصادقة

هناك نوعان من المستخدمين في Kubernetes:

  • حسابات الخدمة - الحسابات المدارة بواسطة Kubernetes API ؛
  • المستخدمين - إدارة المستخدمين "العاديين" بواسطة خدمات خارجية مستقلة.

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

من ناحية أخرى ، لا يمتلك المستخدمون العاديون إدخالات في Kubernetes API: يجب إدارتها بواسطة آليات خارجية. إنها مخصصة للأشخاص أو العمليات التي تعيش خارج المجموعة.

يرتبط كل طلب واجهة برمجة تطبيقات إما بحساب خدمة أو مستخدم ، أو يعتبر مجهول الهوية.

تتضمن بيانات مصادقة المستخدم ما يلي:

  • اسم المستخدم - اسم المستخدم (حساس لحالة الأحرف!) ؛
  • UID - سلسلة تعريف مستخدم يمكن قراءتها آليًا تكون "أكثر اتساقًا وفريدة من نوعها من اسم المستخدم" ؛
  • المجموعة - قائمة المجموعات التي ينتمي إليها المستخدم ؛
  • إكسترا - الحقول الإضافية التي يمكن أن تستخدمها آلية التفويض.

يمكن أن تستخدم Kubernetes عددًا كبيرًا من آليات المصادقة: شهادات X509 ، وعلامات Bearer ، ووكيل المصادقة ، ومصادقة HTTP الأساسية. باستخدام هذه الآليات ، يمكنك تنفيذ عدد كبير من أنظمة التفويض: من ملف كلمة مرور ثابتة إلى OpenID OAuth2.

علاوة على ذلك ، يمكن استخدام مخططات ترخيص متعددة في نفس الوقت. بشكل افتراضي ، تستخدم الكتلة:

  • رموز حساب الخدمة - لحسابات الخدمة ؛
  • X509 - للمستخدمين.

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

شهادات للمستخدمين (X.509)

تتضمن الطريقة التقليدية للتعامل مع الشهادات ما يلي:

  • توليد المفتاح:
    mkdir -p ~/mynewuser/.certs/
    openssl genrsa -out ~/.certs/mynewuser.key 2048
  • إنشاء طلب شهادة:
    openssl req -new -key ~/.certs/mynewuser.key -out ~/.certs/mynewuser.csr -subj "/CN=mynewuser/O=company"
  • معالجة طلب الشهادة باستخدام مفاتيح CA لمجموعة Kubernetes ، والحصول على شهادة مستخدم (للحصول على شهادة ، تحتاج إلى استخدام حساب لديه حق الوصول إلى مفتاح Kubernetes العنقودي CA ، والذي يقع افتراضيًا في /etc/kubernetes/pki/ca.key):
    openssl x509 -req -in ~/.certs/mynewuser.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out ~/.certs/mynewuser.crt -days 500
  • إنشاء ملف التكوين:
    • وصف المجموعة (حدد عنوان وموقع ملف شهادة المرجع المصدق لتثبيت الكتلة المحدد):
      kubectl config set-cluster kubernetes --certificate-authority=/etc/kubernetes/pki/ca.crt --server=https://192.168.100.200:6443
    • أو كيف لاالخيار الموصى به - لا يمكنك تحديد شهادة الجذر (ثم لن يتحقق kubectl من صحة خادم api للكتلة):
      kubectl config set-cluster kubernetes  --insecure-skip-tls-verify=true --server=https://192.168.100.200:6443
    • إضافة مستخدم إلى ملف التكوين:
      kubectl config set-credentials mynewuser --client-certificate=.certs/mynewuser.crt  --client-key=.certs/mynewuser.key
    • إضافة السياق:
      kubectl config set-context mynewuser-context --cluster=kubernetes --namespace=target-namespace --user=mynewuser
    • تعيين السياق الافتراضي:
      kubectl config use-context mynewuser-context

بعد التلاعب أعلاه ، في الملف .kube/config سيتم إنشاء تكوين العرض:

apiVersion: v1
clusters:
- cluster:
    certificate-authority: /etc/kubernetes/pki/ca.crt
    server: https://192.168.100.200:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    namespace: target-namespace
    user: mynewuser
  name: mynewuser-context
current-context: mynewuser-context
kind: Config
preferences: {}
users:
- name: mynewuser
  user:
    client-certificate: /home/mynewuser/.certs/mynewuser.crt
    client-key: /home/mynewuser/.certs/mynewuser.key

لتسهيل نقل التكوين بين الحسابات والخوادم ، من المفيد تحرير قيم المفاتيح التالية:

  • certificate-authority
  • client-certificate
  • client-key

للقيام بذلك ، يمكنك تشفير الملفات المحددة فيها باستخدام base64 وتسجيلها في التكوين ، وإضافة اللاحقة إلى اسم المفاتيح -data، أي. تلقى certificate-authority-data إلخ

شهادات مع kubeadm

مع الإصدار Kubernetes 1.15 تحديث أصبح العمل مع الشهادات أسهل بكثير بفضل الإصدار ألفا من دعمه في فائدة kubeadm. على سبيل المثال ، إليك ما قد يبدو عليه إنشاء ملف التكوين باستخدام مفاتيح المستخدم الآن:

kubeadm alpha kubeconfig user --client-name=mynewuser --apiserver-advertise-address 192.168.100.200

NB: مطلوب عنوان الإعلان يمكن عرضها في تكوين خادم api ، والذي يقع افتراضيًا في ملف /etc/kubernetes/manifests/kube-apiserver.yaml.

ستتم طباعة التكوين الناتج على stdout. يجب أن تبقى في ~/.kube/config حساب المستخدم أو إلى ملف محدد في متغير البيئة KUBECONFIG.

أحفر أكثر عمقا

بالنسبة لأولئك الذين يريدون إلقاء نظرة فاحصة على القضايا الموضحة:

ترخيص

الحساب المرخص له ، بشكل افتراضي ، ليس لديه حقوق التصرف على الكتلة. لمنح الأذونات ، تنفذ Kubernetes آلية تفويض.

قبل الإصدار 1.6 ، استخدم Kubernetes نوع ترخيص يسمى ABAC (التحكم في الوصول المستند إلى السمات). يمكن العثور على تفاصيل حول هذا الموضوع في الوثائق الرسمية. يعتبر هذا الأسلوب حاليًا قديمًا ، ولكن لا يزال بإمكانك استخدامه في نفس الوقت مع أنواع التخويل الأخرى.

الطريقة الفعلية (والأكثر مرونة) لفصل حقوق الوصول إلى الكتلة تسمى RBAC (التحكم في الوصول المستند إلى الدور). تم إعلان أنه مستقر منذ الإصدار Kubernetes 1.8 تحديث. ينفذ RBAC نموذج حقوق الذي لا يسمح بكل شيء غير مسموح به صراحة.
لتمكين RBAC، تحتاج إلى بدء خادم Kubernetes api-server بالمعامل --authorization-mode=RBAC. يتم تعيين المعلمات في البيان مع تكوين خادم api ، والذي يقع افتراضيًا على طول المسار /etc/kubernetes/manifests/kube-apiserver.yaml، في قسم command. ومع ذلك ، بشكل افتراضي ، تم تمكين RBAC بالفعل ، لذلك على الأرجح لا داعي للقلق بشأنه: يمكنك التحقق من ذلك من خلال القيمة authorization-mode (في ما سبق ذكره kube-apiserver.yaml). بالمناسبة ، قد تكون هناك أنواع أخرى من التفويضات من بين قيمها (node, webhook, always allow) ، لكننا سنترك اعتبارهم خارج نطاق المادة.

بالمناسبة ، لقد نشرنا بالفعل статью مع قصة مفصلة إلى حد ما حول مبادئ وميزات العمل مع RBAC ، لذلك سأقتصر على قائمة مختصرة بالأساسيات والأمثلة.

تُستخدم كيانات واجهة برمجة التطبيقات التالية للتحكم في الوصول إلى Kubernetes عبر RBAC:

  • Role и ClusterRole - الأدوار التي تستخدم لوصف حقوق الوصول:
  • Role يسمح لك بوصف الحقوق داخل مساحة الاسم ؛
  • ClusterRole - داخل المجموعة ، بما في ذلك الكائنات الخاصة بالكتلة مثل العقد ، وعناوين url غير المتعلقة بالموارد (أي ، لا تتعلق بموارد Kubernetes - على سبيل المثال ، /version, /logs, /api*);
  • RoleBinding и ClusterRoleBinding - يستخدم للتجليد Role и ClusterRole لمستخدم أو مجموعة مستخدمين أو ServiceAccount.

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

تصف الأدوار الحقوق باستخدام مجموعات من القواعد تحتوي على:

  • مجموعات API - انظر الوثائق الرسمية بواسطة apiGroups والإخراج kubectl api-resources;
  • موارد (موارد: pod, namespace, deployment وما إلى ذلك وهلم جرا.)؛
  • أفعال (الأفعال: set, update الخ).
  • أسماء الموارد (resourceNames) - في الحالة التي تحتاج فيها إلى توفير الوصول إلى مورد معين، وليس إلى جميع الموارد من هذا النوع.

يمكن العثور على مزيد من التفاصيل التفصيلية للتفويض في Kubernetes على الصفحة الوثائق الرسمية. بدلاً من ذلك (أو بالأحرى بالإضافة إلى ذلك) ، سأقدم أمثلة توضح عملها.

أمثلة على كيان RBAC

بسيط Role، والذي يسمح لك بالحصول على قائمة وحالة البودات وتتبعها في مساحة الاسم target-namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: target-namespace
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

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

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  # секции "namespace" нет, так как ClusterRole задействует весь кластер
  name: secret-reader
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "watch", "list"]

مثال RoleBindingالذي يسمح للمستخدم mynewuser قرون "قراءة" في مساحة الاسم my-namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: target-namespace
subjects:
- kind: User
  name: mynewuser # имя пользователя зависимо от регистра!
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role # здесь должно быть “Role” или “ClusterRole”
  name: pod-reader # имя Role, что находится в том же namespace,
                   # или имя ClusterRole, использование которой
                   # хотим разрешить пользователю
  apiGroup: rbac.authorization.k8s.io

تدقيق الحدث

من الناحية التخطيطية ، يمكن تمثيل بنية Kubernetes على النحو التالي:

ABC للأمان في Kubernetes: المصادقة ، التفويض ، التدقيق

مكون Kubernetes الرئيسي المسؤول عن معالجة الطلبات هو - خادم واجهة برمجة التطبيقات. تمر جميع العمليات على الكتلة من خلاله. يمكنك قراءة المزيد حول هذه الآليات الداخلية في المقالة "ماذا يحدث في Kubernetes عند تشغيل kubectl run؟".

يعد System Auditing ميزة مثيرة للاهتمام في Kubernetes يتم تعطيلها افتراضيًا. يسمح لك بتسجيل جميع المكالمات إلى Kubernetes API. كما قد تتخيل ، يتم تنفيذ جميع الإجراءات المتعلقة بمراقبة حالة الكتلة وتغييرها من خلال واجهة برمجة التطبيقات هذه. يمكن العثور على وصف جيد لقدراته (كالعادة) في الوثائق الرسمية K8s. فيما يلي سأحاول شرح الموضوع بطريقة أبسط.

وهكذا، لتمكين التدقيق، نحتاج إلى تمرير ثلاث معاملات مطلوبة إلى الحاوية في خادم api ، والتي تم وصفها بمزيد من التفصيل أدناه:

  • --audit-policy-file=/etc/kubernetes/policies/audit-policy.yaml
  • --audit-log-path=/var/log/kube-audit/audit.log
  • --audit-log-format=json

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

  • --audit-log-maxbackup=10
  • --audit-log-maxsize=100
  • --audit-log-maxage=7

لكننا لن نتناولها بمزيد من التفصيل - يمكنك العثور على كل التفاصيل في وثائق kube-apiserver.

كما ذكرنا سابقًا ، يتم تعيين جميع المعلمات في البيان مع تكوين خادم api (افتراضيًا /etc/kubernetes/manifests/kube-apiserver.yaml)، في القسم command. دعنا نعود إلى المعلمات الثلاثة المطلوبة ونحللها:

  1. audit-policy-file - المسار إلى ملف YAML مع وصف سياسة (سياسة) التدقيق. سنعود إلى محتوياته ، لكن في الوقت الحالي سألاحظ أن الملف يجب أن يكون قابلاً للقراءة بواسطة عملية خادم api. لذلك ، تحتاج إلى تثبيته داخل الحاوية ، حيث يمكنك إضافة الكود التالي إلى الأقسام المناسبة من التكوين:
      volumeMounts:
        - mountPath: /etc/kubernetes/policies
          name: policies
          readOnly: true
      volumes:
      - hostPath:
          path: /etc/kubernetes/policies
          type: DirectoryOrCreate
        name: policies
  2. audit-log-path - مسار ملف السجل. يجب أن يكون المسار متاحًا أيضًا لعملية خادم api ، لذلك نصف طريقة تركيبه بالطريقة نفسها:
      volumeMounts:
        - mountPath: /var/log/kube-audit
          name: logs
          readOnly: false
      volumes:
      - hostPath:
          path: /var/log/kube-audit
          type: DirectoryOrCreate
        name: logs
  3. audit-log-format - تنسيق سجل التدقيق. الافتراضي هو json، ولكن تنسيق النص القديم متاح أيضًا (legacy).

نهج التدقيق

الآن حول الملف المذكور مع وصف لسياسة التسجيل. المفهوم الأول لسياسة التدقيق هو level, مستوى التسجيل. وهم على النحو التالي:

  • None - لا تسجل ؛
  • Metadata - بيانات وصفية لطلب السجل: المستخدم ، وقت الطلب ، المورد الهدف (القرص ، مساحة الاسم ، إلخ) ، نوع الإجراء (الفعل) ، إلخ ؛
  • Request - سجل البيانات الوصفية ونص الطلب ؛
  • RequestResponse - سجل البيانات الوصفية وجسم الطلب وهيئة الاستجابة.

آخر مستويينRequest и RequestResponse) لا تسجل الطلبات التي لا تصل إلى الموارد (مراجع لما يسمى بعناوين url غير الموارد).

أيضًا ، يتم تنفيذ جميع الطلبات عدة مراحل:

  • RequestReceived - المرحلة التي يتم فيها استلام الطلب من قبل المعالج ولم يتم نقله بعد على طول سلسلة المتعاملين ؛
  • ResponseStarted - يتم إرسال رؤوس الاستجابة ، ولكن قبل إرسال نص الرد. تم إنشاؤها للاستعلامات طويلة الأمد (على سبيل المثال ، watch);
  • ResponseComplete - تم إرسال هيئة الرد ، ولن يتم إرسال المزيد من المعلومات ؛
  • Panic - يتم إنشاء الأحداث عند اكتشاف حالة غير طبيعية.

لتخطي أي مراحل ، يمكنك استخدام omitStages.

في ملف السياسة ، يمكننا وصف عدة أقسام بمستويات تسجيل مختلفة. سيتم تطبيق قاعدة المطابقة الأولى الموجودة في وصف السياسة.

يستمع برنامج kubelet daemon للتغيير في البيان مع تكوين خادم api ، وإذا كان هناك أي تغيير ، فإنه يعيد تشغيل حاوية خادم api. لكن هناك تفاصيل مهمة: سيتم تجاهل التغييرات في ملف السياسة. بعد إجراء التغييرات على ملف السياسة ، ستحتاج إلى إعادة تشغيل خادم api يدويًا. منذ بدء تشغيل خادم api كملف جراب ثابت، فريق kubectl delete لن تعيد تشغيله. سيتعين عليك القيام بذلك يدويًا docker stop على kube-masters حيث تم تغيير سياسة التدقيق:

docker stop $(docker ps | grep k8s_kube-apiserver | awk '{print $1}')

عند تمكين التدقيق ، من المهم تذكر ذلك يزيد الحمل على kube-apiserver. على وجه الخصوص ، يزيد استهلاك الذاكرة لتخزين سياق الاستعلام. يبدأ التسجيل فقط بعد إرسال عنوان الاستجابة. يعتمد الحمل أيضًا على تكوين سياسة التدقيق.

أمثلة السياسة

دعنا نحلل بنية ملفات السياسة باستخدام الأمثلة.

هنا ملف بسيط policyلتسجيل كل شيء على المستوى Metadata:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata

في السياسة ، يمكنك تحديد قائمة المستخدمين (Users и ServiceAccounts) ومجموعات المستخدمين. على سبيل المثال ، هذه هي الطريقة التي سنتجاهل بها مستخدمي النظام ، ولكننا نسجل كل شيء آخر على المستوى Request:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: None
    userGroups:
      - "system:serviceaccounts"
      - "system:nodes"
    users:
      - "system:anonymous"
      - "system:apiserver"
      - "system:kube-controller-manager"
      - "system:kube-scheduler"
  - level: Request

من الممكن أيضًا وصف الهدف:

  • مساحات الأسماء (namespaces);
  • أفعال (الأفعال: get, update, delete و اخرين)؛
  • موارد (موارد، على النحو التالي: pod, configmaps إلخ) ومجموعات الموارد (apiGroups).

الانتباه! يمكن الحصول على مجموعات الموارد والموارد (مجموعات API ، مثل مجموعات apiGroups) ، بالإضافة إلى إصداراتها المثبتة في الكتلة ، باستخدام الأوامر:

kubectl api-resources
kubectl api-versions

يتم توفير سياسة التدقيق التالية كدليل على أفضل الممارسات في توثيق Alibaba Cloud:

apiVersion: audit.k8s.io/v1beta1
kind: Policy
# Не логировать стадию RequestReceived
omitStages:
  - "RequestReceived"
rules:
  # Не логировать события, считающиеся малозначительными и не опасными:
  - level: None
    users: ["system:kube-proxy"]
    verbs: ["watch"]
    resources:
      - group: "" # это api group с пустым именем, к которому относятся
                  # базовые ресурсы Kubernetes, называемые “core”
        resources: ["endpoints", "services"]
  - level: None
    users: ["system:unsecured"]
    namespaces: ["kube-system"]
    verbs: ["get"]
    resources:
      - group: "" # core
        resources: ["configmaps"]
  - level: None
    users: ["kubelet"]
    verbs: ["get"]
    resources:
      - group: "" # core
        resources: ["nodes"]
  - level: None
    userGroups: ["system:nodes"]
    verbs: ["get"]
    resources:
      - group: "" # core
        resources: ["nodes"]
  - level: None
    users:
      - system:kube-controller-manager
      - system:kube-scheduler
      - system:serviceaccount:kube-system:endpoint-controller
    verbs: ["get", "update"]
    namespaces: ["kube-system"]
    resources:
      - group: "" # core
        resources: ["endpoints"]
  - level: None
    users: ["system:apiserver"]
    verbs: ["get"]
    resources:
      - group: "" # core
        resources: ["namespaces"]
  # Не логировать обращения к read-only URLs:
  - level: None
    nonResourceURLs:
      - /healthz*
      - /version
      - /swagger*
  # Не логировать сообщения, относящиеся к типу ресурсов “события”:
  - level: None
    resources:
      - group: "" # core
        resources: ["events"]
  # Ресурсы типа Secret, ConfigMap и TokenReview могут содержать  секретные данные,
  # поэтому логируем только метаданные связанных с ними запросов
  - level: Metadata
    resources:
      - group: "" # core
        resources: ["secrets", "configmaps"]
      - group: authentication.k8s.io
        resources: ["tokenreviews"]
  # Действия типа get, list и watch могут быть ресурсоёмкими; не логируем их
  - level: Request
    verbs: ["get", "list", "watch"]
    resources:
      - group: "" # core
      - group: "admissionregistration.k8s.io"
      - group: "apps"
      - group: "authentication.k8s.io"
      - group: "authorization.k8s.io"
      - group: "autoscaling"
      - group: "batch"
      - group: "certificates.k8s.io"
      - group: "extensions"
      - group: "networking.k8s.io"
      - group: "policy"
      - group: "rbac.authorization.k8s.io"
      - group: "settings.k8s.io"
      - group: "storage.k8s.io"
  # Уровень логирования по умолчанию для стандартных ресурсов API
  - level: RequestResponse
    resources:
      - group: "" # core
      - group: "admissionregistration.k8s.io"
      - group: "apps"
      - group: "authentication.k8s.io"
      - group: "authorization.k8s.io"
      - group: "autoscaling"
      - group: "batch"
      - group: "certificates.k8s.io"
      - group: "extensions"
      - group: "networking.k8s.io"
      - group: "policy"
      - group: "rbac.authorization.k8s.io"
      - group: "settings.k8s.io"
      - group: "storage.k8s.io"
  # Уровень логирования по умолчанию для всех остальных запросов
  - level: Metadata

مثال جيد آخر لسياسة التدقيق هو الملف الشخصي المستخدم في الحملة العالمية للتعليم.

للاستجابة السريعة لأحداث التدقيق ، من الممكن وصف webhook. تمت تغطية هذا السؤال في الوثائق الرسميةسأتركه خارج نطاق هذا المقال.

نتائج

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

PS

اقرأ أيضًا على مدونتنا:

المصدر: www.habr.com

إضافة تعليق