Das ABC der Sicherheit in Kubernetes: Authentifizierung, Autorisierung, Auditing

Das ABC der Sicherheit in Kubernetes: Authentifizierung, Autorisierung, Auditing

Früher oder später stellt sich beim Betrieb eines Systems die Frage der Sicherheit: Bereitstellung von Authentifizierung, Rechtetrennung, Prüfung und andere Aufgaben. Bereits für Kubernetes erstellt viele Lösungen, mit denen Sie auch in sehr anspruchsvollen Umgebungen die Einhaltung von Standards erreichen können ... Das gleiche Material ist den grundlegenden Aspekten der Sicherheit gewidmet, die in den integrierten Mechanismen von K8s implementiert sind. Erstens wird es für diejenigen nützlich sein, die gerade erst anfangen, sich mit Kubernetes vertraut zu machen – als Ausgangspunkt für das Studium von Sicherheitsfragen.

Authentifizierung

In Kubernetes gibt es zwei Arten von Benutzern:

  • Dienstkonten - Konten, die von der Kubernetes-API verwaltet werden;
  • Nutzer - „normale“ Benutzer, die von externen, unabhängigen Diensten verwaltet werden.

Der Hauptunterschied zwischen diesen Typen besteht darin, dass es in der Kubernetes-API spezielle Objekte für Dienstkonten gibt (sie heißen wie folgt: ServiceAccounts), die an einen Namespace und einen Satz von Autorisierungsdaten gebunden sind, die im Cluster in Objekten vom Typ Secrets gespeichert sind. Solche Benutzer (Dienstkonten) dienen hauptsächlich dazu, Zugriffsrechte auf die Kubernetes-API von Prozessen zu verwalten, die in einem Kubernetes-Cluster ausgeführt werden.

Normale Benutzer hingegen haben keine Einträge in der Kubernetes-API: Sie müssen durch externe Mechanismen verwaltet werden. Sie sind für Personen oder Prozesse gedacht, die außerhalb des Clusters leben.

Jede API-Anfrage ist entweder an ein Dienstkonto oder einen Benutzer gebunden oder gilt als anonym.

Zu den Benutzerauthentifizierungsdaten gehören:

  • Benutzername — Benutzername (Groß-/Kleinschreibung beachten!);
  • UID – eine maschinenlesbare Benutzeridentifikationszeichenfolge, die „konsistenter und einzigartiger als ein Benutzername“ ist;
  • Groups — Liste der Gruppen, denen der Benutzer angehört;
  • Extra - zusätzliche Felder, die vom Autorisierungsmechanismus verwendet werden können.

Kubernetes kann eine große Anzahl von Authentifizierungsmechanismen verwenden: X509-Zertifikate, Bearer-Token, Authentifizierungs-Proxy, HTTP Basic Auth. Mit diesen Mechanismen können Sie eine Vielzahl von Autorisierungsschemata implementieren: von einer statischen Passwortdatei bis hin zu OpenID OAuth2.

Darüber hinaus können mehrere Autorisierungsschemata gleichzeitig verwendet werden. Standardmäßig verwendet der Cluster Folgendes:

  • Dienstkonto-Tokens – für Dienstkonten;
  • X509 – für Benutzer.

Die Frage der Verwaltung von ServiceAccounts geht über den Rahmen dieses Artikels hinaus, und für diejenigen, die mehr über dieses Problem erfahren möchten, empfehle ich, mit zu beginnen Offizielle Dokumentationsseiten. Wir werden uns die Ausgabe von X509-Zertifikaten genauer ansehen.

Zertifikate für Benutzer (X.509)

Die klassische Art, mit Zertifikaten zu arbeiten, umfasst:

  • Schlüsselgenerierung:
    mkdir -p ~/mynewuser/.certs/
    openssl genrsa -out ~/.certs/mynewuser.key 2048
  • Generieren einer Zertifikatsanforderung:
    openssl req -new -key ~/.certs/mynewuser.key -out ~/.certs/mynewuser.csr -subj "/CN=mynewuser/O=company"
  • Verarbeiten der Zertifikatsanforderung mithilfe der CA-Schlüssel des Kubernetes-Clusters und Abrufen eines Benutzerzertifikats (um ein Zertifikat zu erhalten, müssen Sie ein Konto verwenden, das Zugriff auf den Schlüssel der Kubernetes-Cluster-CA hat, der sich standardmäßig in befindet /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
  • Erstellen einer Konfigurationsdatei:
    • Beschreibung des Clusters (geben Sie die Adresse und den Speicherort der CA-Zertifikatdatei der spezifischen Clusterinstallation an):
      kubectl config set-cluster kubernetes --certificate-authority=/etc/kubernetes/pki/ca.crt --server=https://192.168.100.200:6443
    • oder wie nichtempfohlene Option – Sie können das Stammzertifikat nicht angeben (dann prüft kubectl nicht die Richtigkeit des Cluster-API-Servers):
      kubectl config set-cluster kubernetes  --insecure-skip-tls-verify=true --server=https://192.168.100.200:6443
    • Hinzufügen eines Benutzers zur Konfigurationsdatei:
      kubectl config set-credentials mynewuser --client-certificate=.certs/mynewuser.crt  --client-key=.certs/mynewuser.key
    • Kontext hinzufügen:
      kubectl config set-context mynewuser-context --cluster=kubernetes --namespace=target-namespace --user=mynewuser
    • Standardkontextzuweisung:
      kubectl config use-context mynewuser-context

Nach den oben genannten Manipulationen in der Datei .kube/config Die Ansichtskonfiguration wird erstellt:

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

Um die Übertragung der Konfiguration zwischen Konten und Servern zu erleichtern, ist es sinnvoll, die Werte der folgenden Schlüssel zu bearbeiten:

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

Dazu können Sie die darin angegebenen Dateien mit Base64 kodieren und in der Konfiguration registrieren, indem Sie das Suffix an den Namen der Schlüssel anhängen -data, d.h. erhalten certificate-authority-data usw.

Zertifikate mit kubeadm

Mit Freigabe Kubernetes 1.15 Die Arbeit mit Zertifikaten ist dank der Alpha-Version der Unterstützung in viel einfacher geworden kubeadm-Dienstprogramm. So könnte beispielsweise die Generierung einer Konfigurationsdatei mit Benutzerschlüsseln jetzt aussehen:

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

NB: Erforderlich Werbeadresse kann in der API-Server-Konfiguration eingesehen werden, die sich standardmäßig in befindet /etc/kubernetes/manifests/kube-apiserver.yaml.

Die resultierende Konfiguration wird auf stdout gedruckt. Es muss drin bleiben ~/.kube/config Benutzerkonto oder auf eine in einer Umgebungsvariablen angegebene Datei KUBECONFIG.

Grab tiefer

Für diejenigen, die sich die beschriebenen Themen genauer ansehen möchten:

Genehmigung

Ein autorisiertes Konto hat standardmäßig keine Rechte, auf dem Cluster zu agieren. Um Berechtigungen zu erteilen, implementiert Kubernetes einen Autorisierungsmechanismus.

Vor Version 1.6 verwendete Kubernetes einen Autorisierungstyp namens ABAC (Attributbasierte Zugriffskontrolle). Details dazu finden Sie in amtliche Dokumentation. Dieser Ansatz gilt derzeit als veraltet, Sie können ihn jedoch weiterhin gleichzeitig mit anderen Autorisierungstypen verwenden.

Die tatsächliche (und flexiblere) Möglichkeit, Zugriffsrechte auf einen Cluster zu trennen, heißt RBAC (Rollenbasierte Zugriffssteuerung). Es wurde seit der Version als stabil erklärt Kubernetes 1.8. RBAC implementiert ein Rechtemodell, das alles verbietet, was nicht ausdrücklich erlaubt ist.
Um RBAC zu aktivieren, müssen Sie den Kubernetes-API-Server mit dem Parameter starten --authorization-mode=RBAC. Die Parameter werden im Manifest mit der API-Server-Konfiguration festgelegt, die sich standardmäßig entlang des Pfads befindet /etc/kubernetes/manifests/kube-apiserver.yaml, im Abschnitt command. Standardmäßig ist RBAC jedoch bereits aktiviert, sodass Sie sich darüber höchstwahrscheinlich keine Sorgen machen sollten: Sie können dies anhand des Werts überprüfen authorization-mode (im bereits erwähnten kube-apiserver.yaml). Zu seinen Werten können übrigens auch andere Arten von Berechtigungen gehören (node, webhook, always allow), aber wir werden ihre Betrachtung über den Rahmen des Materials hinauslassen.

Übrigens haben wir bereits veröffentlicht Artikel mit einer ziemlich detaillierten Geschichte über die Prinzipien und Merkmale der Arbeit mit RBAC, daher werde ich mich im Folgenden auf eine kurze Auflistung der Grundlagen und Beispiele beschränken.

Die folgenden API-Entitäten werden verwendet, um den Zugriff auf Kubernetes über RBAC zu steuern:

  • Role и ClusterRole - Rollen, die der Beschreibung von Zugriffsrechten dienen:
  • Role ermöglicht es Ihnen, die Rechte innerhalb des Namensraums zu beschreiben;
  • ClusterRole – innerhalb des Clusters, einschließlich Cluster-spezifischer Objekte wie Knoten, Nicht-Ressourcen-URLs (d. h. nicht im Zusammenhang mit Kubernetes-Ressourcen – zum Beispiel /version, /logs, /api*);
  • RoleBinding и ClusterRoleBinding - zum Binden verwendet Role и ClusterRole zu einem Benutzer, einer Benutzergruppe oder einem ServiceAccount.

Die Role- und RoleBinding-Entitäten sind an den Namensraum gebunden, d. h. muss im selben Namensraum liegen. Eine RoleBinding kann jedoch auf eine ClusterRole verweisen, die es Ihnen ermöglicht, einen Satz generischer Berechtigungen zu erstellen und den Zugriff damit zu steuern.

Rollen beschreiben Rechte mithilfe von Regelsätzen, die Folgendes enthalten:

  • API-Gruppen – siehe offizielle Dokumentation von apiGroups und Ausgabe kubectl api-resources;
  • Ressourcen (RESSOURCEN: pod, namespace, deployment usw.);
  • Verben (Verben: set, update usw.).
  • Ressourcennamen (resourceNames) – für den Fall, dass Sie Zugriff auf eine bestimmte Ressource und nicht auf alle Ressourcen dieses Typs gewähren müssen.

Eine detailliertere Aufschlüsselung der Autorisierung in Kubernetes finden Sie auf der Seite amtliche Dokumentation. Stattdessen (oder besser gesagt zusätzlich) werde ich Beispiele geben, die seine Arbeit veranschaulichen.

Beispiele für RBAC-Entitäten

Einfach Role, mit dem Sie eine Liste und den Status von Pods abrufen und diese im Namespace verfolgen können 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"]

Beispiel ClusterRole, mit dem Sie eine Liste und den Status von Pods abrufen und diese im gesamten Cluster überwachen können:

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

Beispiel RoleBinding, was dem Benutzer ermöglicht mynewuser Pods im Namespace „lesen“. 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

Ereignisprüfung

Schematisch lässt sich die Architektur von Kubernetes wie folgt darstellen:

Das ABC der Sicherheit in Kubernetes: Authentifizierung, Autorisierung, Auditing

Die wichtigste Kubernetes-Komponente, die für die Verarbeitung von Anfragen verantwortlich ist, ist − API-Server. Alle Vorgänge im Cluster durchlaufen diesen. Mehr über diese internen Mechanismen können Sie im Artikel „Was passiert in Kubernetes, wenn Sie kubectl run ausführen?".

System Auditing ist eine interessante Funktion in Kubernetes, die standardmäßig deaktiviert ist. Damit können Sie alle Aufrufe der Kubernetes-API protokollieren. Wie Sie sich vorstellen können, werden alle Aktionen im Zusammenhang mit der Steuerung und Änderung des Clusterstatus über diese API ausgeführt. Eine gute Beschreibung seiner Fähigkeiten finden Sie (wie üblich) in amtliche Dokumentation K8s. Im Folgenden werde ich versuchen, das Thema einfacher zu erklären.

somit um die Prüfung zu ermöglichen, müssen wir drei erforderliche Parameter an den Container im API-Server übergeben, die im Folgenden ausführlicher beschrieben werden:

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

Zusätzlich zu diesen drei erforderlichen Parametern gibt es viele zusätzliche Einstellungen im Zusammenhang mit der Überwachung: von der Protokollrotation bis hin zu Webhook-Beschreibungen. Beispiel für Protokollrotationsparameter:

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

Wir werden jedoch nicht näher darauf eingehen – alle Details finden Sie in kube-apiserver-Dokumentation.

Wie bereits erwähnt, werden alle Parameter im Manifest mit der API-Server-Konfiguration festgelegt (standardmäßig). /etc/kubernetes/manifests/kube-apiserver.yaml), im Bereich command. Kehren wir zu den drei erforderlichen Parametern zurück und analysieren sie:

  1. audit-policy-file - der Pfad zur YAML-Datei mit einer Beschreibung der Richtlinie (Richtlinie) des Audits. Wir werden auf den Inhalt zurückkommen, aber zunächst möchte ich darauf hinweisen, dass die Datei vom API-Server-Prozess lesbar sein muss. Daher müssen Sie es im Container mounten, wofür Sie den folgenden Code zu den entsprechenden Abschnitten der Konfiguration hinzufügen können:
      volumeMounts:
        - mountPath: /etc/kubernetes/policies
          name: policies
          readOnly: true
      volumes:
      - hostPath:
          path: /etc/kubernetes/policies
          type: DirectoryOrCreate
        name: policies
  2. audit-log-path - Pfad zur Protokolldatei. Der Pfad muss auch für den API-Server-Prozess verfügbar sein, daher beschreiben wir seine Bereitstellung auf die gleiche Weise:
      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 – Audit-Log-Format. Die Standardeinstellung ist json, aber das alte Textformat ist auch verfügbar (legacy).

Audit-Richtlinie

Nun zur erwähnten Datei mit einer Beschreibung der Protokollierungsrichtlinie. Das erste Konzept einer Überwachungsrichtlinie ist level, Protokollierungsstufe. Sie sind wie folgt:

  • None - nicht anmelden;
  • Metadata — Metadaten der Protokollanforderung: Benutzer, Anforderungszeit, Zielressource (Pod, Namespace usw.), Aktionstyp (Verb) usw.;
  • Request - Protokollmetadaten und Anforderungstext;
  • RequestResponse - Protokollmetadaten, Anforderungstext und Antworttext.

Die letzten beiden EbenenRequest и RequestResponse) protokollieren keine Anfragen, die nicht auf Ressourcen zugegriffen haben (Verweise auf die sogenannten Nicht-Ressourcen-URLs).

Außerdem werden alle Anfragen bearbeitet mehrere Stufen:

  • RequestReceived - die Phase, in der die Anfrage beim Handler eingeht und noch nicht weiter entlang der Handlerkette weitergeleitet wurde;
  • ResponseStarted – Die Antwortheader werden gesendet, jedoch bevor der Antworttext gesendet wird. Wird für lang laufende Abfragen generiert (z. B. watch);
  • ResponseComplete - Der Antworttext wurde gesendet, es werden keine weiteren Informationen gesendet;
  • Panic — Ereignisse werden generiert, wenn eine ungewöhnliche Situation erkannt wird.

Um beliebige Stufen zu überspringen, können Sie verwenden omitStages.

In der Richtliniendatei können wir mehrere Abschnitte mit unterschiedlichen Protokollierungsstufen beschreiben. Es wird die erste in der Richtlinienbeschreibung gefundene Übereinstimmungsregel angewendet.

Der Kubelet-Daemon wartet auf eine Änderung im Manifest mit der API-Server-Konfiguration und startet gegebenenfalls den API-Server-Container neu. Aber es gibt ein wichtiges Detail: Änderungen in der Richtliniendatei werden ignoriert. Nachdem Sie Änderungen an der Richtliniendatei vorgenommen haben, müssen Sie den API-Server manuell neu starten. Da der API-Server als gestartet ist statischer Pod, Mannschaft kubectl delete wird es nicht neu starten. Muss es manuell machen docker stop auf Kube-Mastern, bei denen die Prüfrichtlinie geändert wurde:

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

Bei der Aktivierung der Prüfung ist es wichtig, dies zu bedenken Last steigt auf kube-apiserver. Insbesondere steigt der Speicherverbrauch für die Speicherung des Abfragekontexts. Die Protokollierung beginnt erst, nachdem der Antwortheader gesendet wurde. Die Auslastung hängt auch von der Konfiguration der Audit-Richtlinie ab.

Beispiele für Richtlinien

Lassen Sie uns die Struktur von Richtliniendateien anhand von Beispielen analysieren.

Hier ist eine einfache Datei policyum alles auf der Ebene zu protokollieren Metadata:

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

In der Richtlinie können Sie eine Liste von Benutzern angeben (Users и ServiceAccounts) und Benutzergruppen. So ignorieren wir beispielsweise Systembenutzer, protokollieren aber alles andere auf der Ebene 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

Es ist auch möglich, das Ziel zu beschreiben:

  • Namensräume (namespaces);
  • Verben (Verben: get, update, delete und andere);
  • Ressourcen (RESSOURCEN, Wie folgt: pod, configmaps usw.) und Ressourcengruppen (apiGroups).

Achten Sie! Ressourcen und Ressourcengruppen (API-Gruppen, d. h. apiGroups) sowie deren im Cluster installierte Versionen können mit den folgenden Befehlen abgerufen werden:

kubectl api-resources
kubectl api-versions

Die folgende Prüfrichtlinie dient als Demonstration der Best Practices in Alibaba Cloud-Dokumentation:

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

Ein weiteres gutes Beispiel für eine Überwachungsrichtlinie ist Profil, das in GCE verwendet wird.

Für eine schnelle Reaktion auf Audit-Ereignisse ist es möglich Webhook beschreiben. Diese Frage wird in behandelt amtliche DokumentationIch werde es außerhalb des Rahmens dieses Artikels belassen.

Ergebnisse

Der Artikel bietet einen Überblick über die grundlegenden Sicherheitsmechanismen in Kubernetes-Clustern, die es ermöglichen, personalisierte Benutzerkonten zu erstellen, ihre Rechte zu trennen und ihre Aktionen zu protokollieren. Ich hoffe, dass es für diejenigen nützlich sein wird, die in der Theorie oder bereits in der Praxis mit solchen Problemen konfrontiert sind. Ich empfehle Ihnen außerdem, sich mit der Liste anderer Materialien zum Thema Sicherheit in Kubernetes vertraut zu machen, die in „PS“ enthalten ist. Vielleicht finden Sie darin die notwendigen Details zu den für Sie relevanten Problemen.

PS

Lesen Sie auch auf unserem Blog:

Source: habr.com

Kommentar hinzufügen