Nahaufnahme einer Person, die mit einem Tablet auf einer Decke sitzt.

TechWiese Blog

Zugriff auf Azure KeyVault mit dapr Secretstore und aad-pod-identity

18. August 2020

Portrait Bild von Andreas Mock

Andreas Mock

Dieser Blogbeitrag ist ein Repost und stammt im Original aus dem Know-how-Bereich von TechWiese, dessen Artikel in diesem Blog aufgegangen sind.

In diesem Artikel möchte ich Ihnen zeigen, wie Sie mit aad-pod-identity und dapr auf Secrets zugreifen, die in Azure KeyVault gespeichert sind. Azure KeyVault ermöglicht es Ihnen, Secrets zentralisiert zu speichern und ihre Verteilung zu kontrollieren. Azure KeyVault verringert dabei die Wahrscheinlichkeit, dass Secrets versehentlich weitergegeben werden. Bei der Verwendung von Azure KeyVault brauchen Anwendungsentwickler keine Secrets mehr in der Anwendung oder Anwendungskonfiguration zu speichern.

Nehmen wir mal an, eine Anwendung muss eine Verbindung zu einer Datenbank herstellen. Anstatt die Verbindungszeichenfolge im Code der Anwendung oder in einer Konfigurationsdatei zu speichern, kann die Verbindungszeichenfolge sicher in einem KeyVault abgelegt werden. Anwendungsentwickler können dann über die API des Azure KeyVault unter Verwendung eines gültigen Identity-Tokens auf die Verbindungszeichenfolge zugreifen. Ein Identity-Token kann entweder über eine Azure Managed Identity oder einen Service-Principal erworben werden.

Anstatt den Service-Principal eines Azure Active Directory selbst zu verwalten, ziehe ich die Verwendung einer Managed Identity vor. Eine Managed Identity ist ein Feature des Azure Active Directory. Mit diesem Feature brauchen Sie keine Passwörter mehr zu verwalten oder zu erneuern, falls diese nach einiger Zeit abgelaufen sind. Wenn Sie mehr über Managed Identity erfahren möchten, schauen Sie hier in der Azure Dokumentation nach, um mehr Details zu erhalten.

Eine Azure Managed Identity wird als eine Azure-Ressource abgebildet, die Sie innerhalb einer Ressourcengruppe erstellen können. Sie können RBAC-Rollen zuweisen und im Namen der Managed Identity auf Azure Resourcen zuzugreifen. Sie können zum Beispiel Leserechte zuweisen, um einer Managed Identity den Zugriff auf Secrets eines Azure KeyVault zu ermöglichen. Innerhalb Ihres Codes können Sie im Namen der Managed Identity ein Token erwerben und programmgesteuert auf den KeyVault zugreifen.

Aad-Pod-Identity ist ein Kubernetes-Controller, mit dem Sie einem Kubernetes Pod eine Managed Identity zuweisen können. Mit dieser Zuweisung ist es innerhalb Ihres Anwendungscodes möglich, durch Aufruf des Managed Identity Service Endpunkts ein Token zu erhalten. Azure stellt den Managed Identity Service Endpunkt auf VMs bereit und ermöglicht dadurch ein Token für eine Managed Identity zu erwerben.

Dapr Secretstore geht sogar noch einen Schritt weiter. Das dapr-Sidecar ermöglicht es ihnen, Secrets aus einem Azure KeyVault zu lesen, ohne ein Token selbst programmatisch zu erwerben. Wenn Sie einen dapr Secretstore definieren, ruft dapr den Managed Identity Service Endpunkt auf, um ein Token für den Zugriff auf den Azure KeyVault zu erwerben. Sie müssen lediglich die URI des KeyVaults angeben, auf den zugegriffen werden soll.

Das folgende YAML definiert eine dapr Secretstore Komponente vom Typen Azure KeyVault:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: azurekeyvault
spec:
  type: secretstores.azure.keyvault
  metadata:
    - name: vaultName
    value: mykeyvault.vault.azure.net

Wie sie sehen, muss lediglich der Endpunkt der zu verwendenden Azure KeyVault Instanz angegeben werden.

Woher weiß der dapr Sidecar aber nun, welche Identität genutzt werden soll, um ein Token für den API Aufruf an den Azure KeyVault zu erwerben? Hier kommt aad-pod-identity ins Spiel.

Aad-pod-identity ist ein Kubernetes Controller der sicherstellt, dass einem Kubernetes Pod eine Managed Identity zugewiesen wird. Ruft ein Kubernetes Pod den Managed Identity Service Endpunkt auf, um ein Token zu erwerben, stellt dieser Controller sicher, dass ein Token für die zugewiesene Managed Identity erworben wird. Aufrufe aus einem Pod an den Managed Identity Service Endpunkt werden von aad-pod-identity auf ein DaemonSet namens Node Managed Identity umgeleitet. Dieses DaemonSet erkennt den aufrufenden Pod und kann somit die Zuordnung zu einer Managed Identity herstellen und ein Token über den Controller (Managed Identity Controller) anfordern. Es müssen lediglich zwei Kubernetes Resourcen erstellt werden. Die Resource AzureIdentity legt die zu verwendende verwaltete Identität fest:

apiVersion: "aadpodidentity.k8s.io/v1"
kind: AzureIdentity
metadata:
  name: myidentityname
spec:
  type: 0
  resourceID: <managed identity's resource id>
  clientID: <managed identity's client id>

AzureIdentityBinding definiert einen Selector der als Label in einem Pod zu verwenden ist, um die Zuweisung zu einer Managed Identity herzustellen:

apiVersion: "aadpodidentity.k8s.io/v1"
kind: AzureIdentityBinding
metadata:
  name: myidentityname-binding
spec:
  azureIdentity: myidentityname
  selector: myidentityname

Am Ende muss eine Pod Definition nur das Label aadpodidbinding mit dem Wert des Selectors festlegen, um den Pod an eine Managed Identity zu binden:

apiVersion: v1
kind: Pod
metadata:
  name: demo
  labels:
    aadpodidbinding: myidentityname

Das Open Source Projekt aad-pod-identity auf GitHub: https://github.com/Azure/aad-pod-identity

Demo Szenario

Nachdem die Funktionsweise von aad-pod-identity beschrieben wurde, möchte Ich anhand eines Besipiels erläutern, wie man aad-pod-identity und eine dapr Secretstore Komponente in einem AKS Cluster aufsetzt und Secrets über den dapr Sidecar programmatisch abfrägt. Ich werde in diesem Artikel nicht auf jedes Detail und die einzelnen Schritte eingehen, sondern nur beschreiben wie das ganze funktioniert. Ich habe aber innerhalb des aks GitHub Repository der Azure Dev College Organisation alle notwendigen Schritte beschrieben, um das Demo Szenario aufzubauen. Sie finden diesen Beitrag hier.

Das Demo Szenario verwendet eine Azure Managed Identity, welche Leserechte auf einen Azure KeyVault besitzt. Nachdem aad-pod-identity und die dapr Runtime in einem AKS Cluster aufgesetzt wurden, können, wie oben beschrieben, die notwendigen Kubernetes Resourcen erstellt werden. Eine bereits implementierte Anwendung als API in ASP.NET Core wird in dem Cluster mit aktiviertem dapr Sidecar bereitgestellt. Die einzige Aufgabe der Anwendung ist es zwei Secrets secretone und secrettwo aus dem Azure KeyVault abzufragen und zurückzugeben. Für das Abfragen der Secrets wird jeweils eine Http Anfrage an den dapr Sidecar abgesetzt.

Overview

Wie man in dem Schaubild erkennt, greift die Anwendung nur über den dapr Sidecar auf den Azure KeyVault zu. Es sind keine Integrationsbibliotheken für den Zugriff auf einen Azure KeyVault notwendig, da die Abfragen über einfache Http Aufrufe an den dapr Sidecar realisiert werden. Der dapr Sidecar realisiert den Zugriff auf die Azure KeyVault Instanz und das Erwerben eines gültigen Identity-Token. Für das Erwerben eines gültigen Token stellt der dapr-Sidecar eine Tokenanfrage an den Managed Identity Service Endpunkt. Hier greift dann letztendlich aad-pod-identity ein und stellt sicher, dass ein Token für die zugewiesene Managed Identiy der Anwendung verwendet wird.

Wirft man einen Blick auf die Deployment Definition der Anwendung erkennt man, dass die zu verwendende Identität über das Label aadpodidbinding angegeben wird und der dapr Sidecar über Annotations aktiviert wird:

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: dapr-secrets
  name: api-aspnetcore
  labels:
    app: dapr-secrets
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dapr-secrets
      api: aspnetcore
  template:
    metadata:
      labels:
        app: dapr-secrets
        api: aspnetcore
        aadpodidbinding: $IDENTITY_NAME
      annotations:
        dapr.io/enabled: "true"
        dapr.io/id: "api-aspnetcore"
        dapr.io/port: "5000"
    spec:
      containers:
        - name: api-aspnetcore
          image: m009/dapr-secret-api-dotnetcore:0.1
          ports:
            - containerPort: 5000
          imagePullPolicy: Always

Die Demo-Anwendung ruft einfach den dapr-Sidecar auf, um ein Secret nach dem anderen abzufragen, und gibt die Werte zurück. Der dapr-Sidecar hört auf Port 3500 und die Secrets werden durch Angabe der folgenden Url gelesen:

http://localhost:{_daprPort}/v1.0/secrets/{_secretStoreName}/{_secretName}

Der Pfadparameter _secretStoreName ist der Name der dapr Secretstore Komponente, der in folgender Definition als azurekeyvault hinterlegt ist:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  namespace: dapr-secrets
  name: azurekeyvault
spec:
  type: secretstores.azure.keyvault
  metadata:
    - name: vaultName
      value: mykeyvault.vault.azure.net

Der Pfadparameter _secretName ist der Name des Secrets, das aus der Azure KeyVault Instanz gelesen wird. Der folgende Code veranschaulicht alle notwendigen Schritte, um über den dapr Sidecar auf den Azure KeyVault zuzugreifen. Es muss kein Code geschrieben werden um sich ein gültiges Token zu besorgen. Auch die Azure KeyVault API wird nicht direkt angesprochen.

public class SecretController : ControllerBase 
    {
        private static int _daprPort = 3500;
        private static string _secretsUrl = $"http://localhost:{_daprPort }/v1.0/secrets";
        private static string _secretStoreName = "azurekeyvault";
        private static string _secretOne = "secretone";
        private static string _secretTwo = "secrettwo";

        [HttpGet]
        public async Task<IActionResult> GetSecrets()
        {
            try 
            {
                var client = new HttpClient();
                var result = awaitclient .GetAsync($"{_secretsUrl}/{_secretStoreName}/{_secretOne}");

                if (!result.IsSuccessStatusCode)
                {
                    return StatusCode((int)HttpStatusCode.InternalServerError);
                }

                var json = await result.Content.ReadAsStringAsync();
                System.Console.WriteLine(json);
                var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
                var secretOne = dict[_secretOne];

                result = await client.GetAsync($"{_secretsUrl }/{_secretStoreName }/{_secretTwo}");

                if (!result.IsSuccessStatusCode)
                {
                    return StatusCode((int)HttpStatusCode.InternalServerError);
                }

                json = await result.Content.ReadAsStringAsync();
                dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
                var secretTwo = dict[_secretTwo];

                return Ok($"Result from aspnetcore API: SecretOne: {secretOne} | SecretTwo: {secretTwo}");
            }
            catch  (Exception ex)
            {
                return Ok(ex.Message);
            }
        }
    }

Für Anwendungsentwickler ist es sehr angenehm keinen Authentifizierungscode schreiben zu müssen. Aus Erfahrung ist dies meist ein größeres Hindernis. Dapr fungiert hier als Adapter zwischen dem Anwendungscode und dem Azure KeyVault. Durch das Auslagern in einen Sidecar und dem Zugriff über Http müssen keine weiteren Integrationsbibilotheken verwendet werden. Natürlich ist ASP.NET Core und C# nicht das einzige Framework bzw. Programmiersprache, um Anwendungen bereitzustellen. Um die Sache rund zu machen, habe ich eine zweite API, die in Go implementiert ist bereitgestellt. Auch hier ist es aber genau dasselbe Vorgehen. Die Anwendung wird in Kubernetes mit aktiviertem dapr Sidecar bereitgestellt und es kann auch hier über Http Anfragen der Azure KeyVault abgefragt werden, ohne dabei Authentifizierungscode zu schreiben:

const (
        ...
        daprSecretURL   = "http://localhost:3500/v1.0/secrets"
        secretStoreName = "azurekeyvault"
        secretOne       = "secretone"
        secretTwo       = "secretTwo"
)
...

func (s*api) onGetSecrets(c*routing.Context) error {
        baseURL := fmt.Sprintf("%s/%s", daprSecretURL, secretStoreName)

        secretOneURL := fmt.Sprintf("%s/%s", baseURL, secretOne)
        secretTwoURL := fmt.Sprintf("%s/%s", baseURL, secretTwo)

        // query first value
        resp, err := http.Get(secretOneURL)

        if err != nil {
              c.Response.SetStatusCode(500)
              c.Response.SetBody([]byte(err.Error()))
              return err
        }

        defer resp.Body.Close()

        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
              c.Response.SetStatusCode(500)
              c.Response.SetBody([]byte(err.Error()))
              return err
        }

        tmp := make(map[string]string)
        err = json.Unmarshal(body, &tmp)
        if err != nil {
              c.Response.SetStatusCode(500)
              c.Response.SetBody([]byte(err.Error()))
              return err
        }

        vauleOne := tmp[secretOne]

        // query second value
        resp2, err := http.Get(secretTwoURL)

        if err != nil {
              c.Response.SetStatusCode(500)
              c.Response.SetBody([]byte(err.Error()))
              return err
        }

        defer resp2.Body.Close()

        body2, err := ioutil.ReadAll(resp2.Body)
        if err != nil {
              c.Response.SetStatusCode(500)
              c.Response.SetBody([]byte(err.Error()))
              return err
        }

        tmp = make(map[string]string)
        err = json.Unmarshal(body2, &tmp)
        if err != nil {
              c.Response.SetStatusCode(500)
              c.Response.SetBody([]byte(err.Error()))
              return err
        }

        vauleTwo := tmp[secretTwo]

        result := fmt.Sprintf("Result from Go API: secretOne %s | secretTwo: %s", vauleOne, vauleTwo)

        c.Response.SetStatusCode(200)
        c.Response.SetBody([]byte(result))
        return nil
}

Dapr Secretstore nutzen, um Zugriff auf Azure Resourcen für dapr Komponenten zu gewähren

Der dapr Secretstore kann nicht nur innerhalb Ihres Codes verwendet werden, um Secrets zu lesen. Sie können ihn auch in anderen dapr Komponenten-Definitionen selbst verwenden, um den Zugriff auf Azure-Ressourcen zu autorisieren. Stellen Sie sich vor, sie möchten eine dapr Binding Komponente auf der Basis von Azure ServiceBus Queues erstellen. Um Zugriff auf die Azure ServiceBus Queue zu gewähren, müssen Sie den SharedAccessKey Ihrer ServiceBus Instanz angeben. Natürlich wollen Sie diesen Schlüssel nicht als Klartext in der Komponenten-Definition speichern. Stattdessen können Sie auf einen dapr Secretstore verweisen und den Namen des zu verwendenden Secrets angeben. Das macht die Sache rund, denn alle Secrets können so im Azure KeyVault gespeichert werden und in den Komponenten Definitionen verbunden und abgefragt werden.

In einem meiner früheren Artikel schrieb ich über dapr Output- und Input-Bindings, und erstellte eine einfache Demo Anwendung, die das Producer und Consumer Problem mit dapr und Azure ServiceBus Queues löst. Um eine dapr Binding Komponente für den Zugriff auf eine Azure ServiceBus Instanz zu autorisieren, verwendete ich Kubernetes Secrets.

Jetzt möchte ich Ihnen zeigen, wie Sie eine dapr Secretstore Komponente auf der Basis von Azure KeyVault verwenden können, um die dapr Bindings Komponente auf der Basis von Azure ServiceBus Queues zu autorisieren.

Die folgende YAML Definition zeigt, wie man eine dapr Binding Komponente für eine Azure ServiceBus Queue erstellt und wie man auf den Zugriffsschlüssel aus einer Azure KeyVault Instanz verweist:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  namespace: dapr-secrets
  name: message-queue
spec:
  type: bindings.azure.servicebusqueues
  metadata:
    - name: connectionString
      secretKeyRef:
        name: asbrootkey
        key: asbrootkey
    - name: queueName
      value: "msgqueue"
auth:
  secretStore: azurekeyvault

Die Angabe der Verbindungszeichenfolge wird über eine Referenz auf einen Secret-Key mit dem Namen asbrootkey angegeben. In dem Bereich auth wird auf den dapr Secretstore azurekeyvault verwiesen. Zur Laufzeit liest die dapr Runtime über den dapr Secretstore und den Namen des Keys asbrootkey den Wert aus der Azure KeyVault Instanz und kann somit die Verbindung zur Azure ServiceBus Instanz herstellen. Der Zugriff auf die Azure KeyVault Instanz wird natürlich unter der Verwendung einer Managed Identity realisiert, wie wir bereits oben gesehen haben.

Dieses Vorgehen ermöglicht es ihnen alle notwendigen Secrets in einem Azure KeyVault abzulegen und über einen Key auf die Werte zu verweisen. Nutzen sie IaC (Infratsructure as Code), können sie alle notwendigen Secrets in Azure KeyVault ablegen und in Kubernetes YAML Definitionen per Key auf die Werte verweisen. Für Anwendungsentwickler ist es nicht mehr notwendig Infrastruktur-abhängigen Code zu schreiben, um auf Secrets zuzugreifen.

Zusamennfassung

Mit Azure KeyVault in einer dapr Secretstore Komponente ist es möglich, alle notwendigen Secrets zentral zu speichern und es kann auch innerhalb von dapr Komponenten-Definition darauf zuzugegriffen werden. Entwickler programmieren nur gegen die dapr Komponenten und müssen keine Secrets im Code aufbewahren oder Code implementieren, der direkt auf einen KeyVault zugreift. Dapr bietet eine Abstraktionsebene, die es ermöglicht ihre Anwendung mit anderen 3rd Party Komponenten oder in anderen Umgebungen auszuliefern, ohne dabei ihren Code anfassen zu müssen. Anwendungsentwickler implementieren fachlichen Code, während sich DevOps Engineers darum kümmern, die Anwendung auf verschiedenen Platformen auszurollen.

Links

Die genannten Beispiele sind hier zum Ausprobieren verfügbar. Alle notwendigen Schritte sind Step by Step beschrieben.