Skip to content

Commit

Permalink
Merge pull request #51 from skriss/aad-pod-identity
Browse files Browse the repository at this point in the history
support aad-pod-identity auth
  • Loading branch information
nrb committed Jun 9, 2020
2 parents 46e5099 + c22efaf commit 15b36f1
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 77 deletions.
112 changes: 96 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ consider using Premium Managed Disks, which are SSD backed.
To set up Velero on Azure, you:

- [Create an Azure storage account and blob container][1]
- [Get the resource group containing your VMs and disks][4]
- [Set permissions for Velero][2]
- [Install and start Velero][3]

Expand Down Expand Up @@ -109,18 +110,7 @@ BLOB_CONTAINER=velero
az storage container create -n $BLOB_CONTAINER --public-access off --account-name $AZURE_STORAGE_ACCOUNT_ID
```

## Set permissions for Velero

There are two ways Velero can authenticate to Azure: (1) by using a Velero-specific [service principal][17]; or (2) by using a storage account access key.

If you plan to use Velero to take Azure snapshots of your persistent volume managed disks, you **must** use the service principal method.

If you don't plan to take Azure disk snapshots, either method is valid.


### Option 1: Create service principal

#### Get resource group containing your VMs/disks
## Get resource group containing your VMs and disks

_(Optional) If you decided to backup to a different Subscription, make sure you change back to the Subscription
of your cluster's resources before continuing._
Expand All @@ -142,6 +132,17 @@ of your cluster's resources before continuing._

Get your cluster's Resource Group name from the `ResourceGroup` value in the response, and use it to set `$AZURE_RESOURCE_GROUP`.

## Set permissions for Velero

There are several ways Velero can authenticate to Azure: (1) by using a Velero-specific [service principal][17]; (2) by using [AAD Pod Identity][20]; or (3) by using a storage account access key.

If you plan to use Velero to take Azure snapshots of your persistent volume managed disks, you **must** use the service principal or AAD Pod Identity method.

If you don't plan to take Azure disk snapshots, any method is valid.


### Option 1: Create service principal

#### Create service principal

1. Obtain your Azure Account Subscription ID and Tenant ID:
Expand Down Expand Up @@ -188,7 +189,81 @@ of your cluster's resources before continuing._
> available `AZURE_CLOUD_NAME` values: `AzurePublicCloud`, `AzureUSGovernmentCloud`, `AzureChinaCloud`, `AzureGermanCloud`
### Option 2: Use storage account access key
### Option 2: Use AAD Pod Identity
These instructions have been adapted from the [aad-pod-identity documentation][21].
Before proceeding, ensure that you have installed and configured [aad-pod-identity][20] for your cluster.
#### Create identity
1. Obtain your Azure Account Subscription ID:
```bash
AZURE_SUBSCRIPTION_ID=`az account list --query '[?isDefault].id' -o tsv`
```
1. Create an identity for Velero:
```bash
export IDENTITY_NAME=velero
az identity create \
--subscription $AZURE_SUBSCRIPTION_ID \
--resource-group $AZURE_RESOURCE_GROUP \
--name $IDENTITY_NAME
export IDENTITY_CLIENT_ID="$(az identity show -g $AZURE_RESOURCE_GROUP -n $IDENTITY_NAME --subscription $SUBSCRIPTION_ID --query clientId -otsv)"
export IDENTITY_RESOURCE_ID="$(az identity show -g $AZURE_RESOURCE_GROUP -n $IDENTITY_NAME --subscription $SUBSCRIPTION_ID --query id -otsv)"
```
If you'll be using Velero to backup multiple clusters with multiple blob containers, it may be desirable to create a unique identity name per cluster rather than the default `velero`.
1. Assign the identity a role:
```bash
export IDENTITY_ASSIGNMENT_ID="$(az role assignment create --role Contributor --assignee $IDENTITY_CLIENT_ID --scope /subscriptions/$SUBSCRIPTION_ID --query id -otsv)"
```
1. In the cluster, create an `AzureIdentity` and `AzureIdentityBinding`:
```bash
cat <<EOF | kubectl apply -f -
apiVersion: "aadpodidentity.k8s.io/v1"
kind: AzureIdentity
metadata:
name: $IDENTITY_NAME
spec:
type: 0
resourceID: $IDENTITY_RESOURCE_ID
clientID: $IDENTITY_CLIENT_ID
EOF
cat <<EOF | kubectl apply -f -
apiVersion: "aadpodidentity.k8s.io/v1"
kind: AzureIdentityBinding
metadata:
name: $IDENTITY_NAME-binding
spec:
azureIdentity: $IDENTITY_NAME
selector: $IDENTITY_NAME
EOF
```
1. Create a file that contains all the relevant environment variables:
```bash
cat << EOF > ./credentials-velero
AZURE_SUBSCRIPTION_ID=${AZURE_SUBSCRIPTION_ID}
AZURE_RESOURCE_GROUP=${AZURE_RESOURCE_GROUP}
AZURE_CLOUD_NAME=AzurePublicCloud
EOF
```
> available `AZURE_CLOUD_NAME` values: `AzurePublicCloud`, `AzureUSGovernmentCloud`, `AzureChinaCloud`, `AzureGermanCloud`
### Option 3: Use storage account access key
_Note: this option is **not valid** if you are planning to take Azure snapshots of your managed disks with Velero._
Expand All @@ -211,11 +286,11 @@ _Note: this option is **not valid** if you are planning to take Azure snapshots
## Install and start Velero
[Download][4] Velero
[Download][6] Velero
Install Velero, including all prerequisites, into the cluster and start the deployment. This will create a namespace called `velero`, and place a deployment named `velero` in it.
**If using service principal:**
**If using service principal or AAD Pod Identity:**
```bash
velero install \
Expand All @@ -227,6 +302,8 @@ velero install \
--snapshot-location-config apiTimeout=<YOUR_TIMEOUT>[,resourceGroup=$AZURE_BACKUP_RESOURCE_GROUP,subscriptionId=$AZURE_BACKUP_SUBSCRIPTION_ID]
```
If you're using **AAD Pod Identity**, you now need to add the `aadpodidbinding=$IDENTITY_NAME` label to the Velero pod(s), preferably through the Deployment's pod template.
**If using storage account access key and no Azure snapshots:**
```bash
Expand All @@ -252,14 +329,17 @@ For more complex installation needs, use either the Helm chart, or add `--dry-ru
[1]: #Create-Azure-storage-account-and-blob-container
[2]: #Set-permissions-for-Velero
[3]: #Install-and-start-Velero
[4]: https://velero.io/docs/install-overview/
[4]: #Get-resource-group-containing-your-VMs-and-disks
[6]: https://velero.io/docs/install-overview/
[7]: backupstoragelocation.md
[8]: volumesnapshotlocation.md
[9]: https://velero.io/docs/customize-installation/
[11]: https://velero.io/docs/faq/
[17]: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-application-objects
[18]: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
[19]: https://docs.microsoft.com/en-us/azure/architecture/best-practices/naming-conventions#storage
[20]: https://github.com/Azure/aad-pod-identity
[21]: https://github.com/Azure/aad-pod-identity#demo
[22]: https://azure.microsoft.com/en-us/services/kubernetes-service/
[101]: https://github.com/vmware-tanzu/velero-plugin-for-microsoft-azure/workflows/Master%20CI/badge.svg
[102]: https://github.com/vmware-tanzu/velero-plugin-for-microsoft-azure/actions?query=workflow%3A"Master+CI"
Empty file added changelogs/unreleased/.keep
Empty file.
1 change: 1 addition & 0 deletions changelogs/unreleased/51-gitirabassi
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add support for `aad-pod-identity` authentication
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.13
require (
github.com/Azure/azure-sdk-for-go v42.0.0+incompatible
github.com/Azure/go-autorest/autorest v0.9.6
github.com/Azure/go-autorest/autorest/adal v0.8.2
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2
github.com/dnaeon/go-vcr v1.0.1 // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/hashicorp/go-hclog v0.9.2 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@ github.com/Azure/azure-sdk-for-go v42.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
github.com/Azure/go-autorest/autorest v0.9.6 h1:5YWtOnckcudzIw8lPPBcWOnmIFWMtHci1ZWAZulMSx0=
github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0=
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 h1:iM6UAvjR97ZIeR93qTcwpKNMpV+/FTWjwEbuPD495Tk=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 h1:LXl088ZQlP0SBppGFsRZonW6hSvwgL5gRByMbvUbx8U=
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=
github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM=
Expand Down Expand Up @@ -80,6 +87,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
Expand Down Expand Up @@ -258,6 +267,7 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
Expand Down
15 changes: 1 addition & 14 deletions velero-plugin-for-microsoft-azure/common.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2018 the Velero contributors.
Copyright 2018, 2020 the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -20,17 +20,13 @@ import (
"os"
"strings"

"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/joho/godotenv"
"github.com/pkg/errors"
)

const (
tenantIDEnvVar = "AZURE_TENANT_ID"
subscriptionIDEnvVar = "AZURE_SUBSCRIPTION_ID"
clientIDEnvVar = "AZURE_CLIENT_ID"
clientSecretEnvVar = "AZURE_CLIENT_SECRET"
cloudNameEnvVar = "AZURE_CLOUD_NAME"

resourceGroupConfigKey = "resourceGroup"
Expand Down Expand Up @@ -60,15 +56,6 @@ func parseAzureEnvironment(cloudName string) (*azure.Environment, error) {
return &env, errors.WithStack(err)
}

func newServicePrincipalToken(tenantID, clientID, clientSecret string, env *azure.Environment) (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, tenantID)
if err != nil {
return nil, errors.Wrap(err, "error getting OAuthConfig")
}

return adal.NewServicePrincipalToken(*oauthConfig, clientID, clientSecret, env.ResourceManagerEndpoint)
}

func getRequiredValues(getValue func(string) string, keys ...string) (map[string]string, error) {
missing := []string{}
results := map[string]string{}
Expand Down
60 changes: 34 additions & 26 deletions velero-plugin-for-microsoft-azure/object_store.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2017, 2019 the Velero contributors.
Copyright 2017, 2019, 2020 the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -25,10 +25,10 @@ import (
"strings"
"time"

storagemgmt "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2018-02-01/storage"
storagemgmt "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"

Expand All @@ -38,7 +38,7 @@ import (
const (
storageAccountConfigKey = "storageAccount"
storageAccountKeyEnvVarConfigKey = "storageAccountKeyEnvVar"
subscriptionIdConfigKey = "subscriptionId"
subscriptionIDConfigKey = "subscriptionId"
blockSizeConfigKey = "blockSizeInBytes"

// blocks must be less than/equal to 100MB in size
Expand Down Expand Up @@ -148,20 +148,30 @@ func newObjectStore(logger logrus.FieldLogger) *ObjectStore {
return &ObjectStore{log: logger}
}

// getSubscriptionID gets the subscription ID from the 'config' map if it contains
// it, else from the AZURE_SUBSCRIPTION_ID environment variable.
func getSubscriptionID(config map[string]string) string {
if subscriptionID := config[subscriptionIDConfigKey]; subscriptionID != "" {
return subscriptionID
}

return os.Getenv(subscriptionIDEnvVar)
}

func getStorageAccountKey(config map[string]string) (string, *azure.Environment, error) {
// load environment vars from $AZURE_CREDENTIALS_FILE, if it exists
if err := loadEnv(); err != nil {
return "", nil, err
}

// 1. get Azure cloud from AZURE_CLOUD_NAME, if it exists. If the env var does not
// get Azure cloud from AZURE_CLOUD_NAME, if it exists. If the env var does not
// exist, parseAzureEnvironment will return azure.PublicCloud.
env, err := parseAzureEnvironment(os.Getenv(cloudNameEnvVar))
if err != nil {
return "", nil, errors.Wrap(err, "unable to parse azure cloud name environment variable")
}

// 2. get storage account key from env var whose name is in config[storageAccountKeyEnvVarConfigKey].
// get storage account key from env var whose name is in config[storageAccountKeyEnvVarConfigKey].
// If the config does not exist, continue obtaining the storage key using API
if secretKeyEnvVar := config[storageAccountKeyEnvVarConfigKey]; secretKeyEnvVar != "" {
storageKey := os.Getenv(secretKeyEnvVar)
Expand All @@ -172,35 +182,33 @@ func getStorageAccountKey(config map[string]string) (string, *azure.Environment,
return storageKey, env, nil
}

// 3. we need AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID
envVars, err := getRequiredValues(os.Getenv, tenantIDEnvVar, clientIDEnvVar, clientSecretEnvVar, subscriptionIDEnvVar)
if err != nil {
return "", nil, errors.Wrap(err, "unable to get all required environment variables")
}

// 4. check whether a different subscription ID was set for backups in config["subscriptionId"]
subscriptionId := envVars[subscriptionIDEnvVar]
if val := config[subscriptionIdConfigKey]; val != "" {
subscriptionId = val
// get subscription ID from object store config or AZURE_SUBSCRIPTION_ID environment variable
subscriptionID := getSubscriptionID(config)
if subscriptionID == "" {
return "", nil, errors.New("azure subscription ID not found in object store's config or in environment variable")
}

// 5. we need config["resourceGroup"], config["storageAccount"]
// we need config["resourceGroup"], config["storageAccount"]
if _, err := getRequiredValues(mapLookup(config), resourceGroupConfigKey, storageAccountConfigKey); err != nil {
return "", env, errors.Wrap(err, "unable to get all required config values")
}

// 6. get SPT
spt, err := newServicePrincipalToken(envVars[tenantIDEnvVar], envVars[clientIDEnvVar], envVars[clientSecretEnvVar], env)
// get authorizer from environment in the following order:
// 1. client credentials (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)
// 2. client certificate (AZURE_CERTIFICATE_PATH, AZURE_CERTIFICATE_PASSWORD)
// 3. username and password (AZURE_USERNAME, AZURE_PASSWORD)
// 4. MSI (managed service identity)
authorizer, err := auth.NewAuthorizerFromEnvironment()
if err != nil {
return "", env, errors.Wrap(err, "error getting service principal token")
return "", nil, errors.Wrap(err, "error getting authorizer from environment")
}

// 7. get storageAccountsClient
storageAccountsClient := storagemgmt.NewAccountsClientWithBaseURI(env.ResourceManagerEndpoint, subscriptionId)
storageAccountsClient.Authorizer = autorest.NewBearerAuthorizer(spt)
// get storageAccountsClient
storageAccountsClient := storagemgmt.NewAccountsClientWithBaseURI(env.ResourceManagerEndpoint, subscriptionID)
storageAccountsClient.Authorizer = authorizer

// 8. get storage key
res, err := storageAccountsClient.ListKeys(context.TODO(), config[resourceGroupConfigKey], config[storageAccountConfigKey])
// get storage key
res, err := storageAccountsClient.ListKeys(context.TODO(), config[resourceGroupConfigKey], config[storageAccountConfigKey], storagemgmt.Kerb)
if err != nil {
return "", env, errors.WithStack(err)
}
Expand Down Expand Up @@ -235,7 +243,7 @@ func (o *ObjectStore) Init(config map[string]string) error {
if err := veleroplugin.ValidateObjectStoreConfigKeys(config,
resourceGroupConfigKey,
storageAccountConfigKey,
subscriptionIdConfigKey,
subscriptionIDConfigKey,
blockSizeConfigKey,
storageAccountKeyEnvVarConfigKey,
); err != nil {
Expand Down

0 comments on commit 15b36f1

Please sign in to comment.