github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/azure/disk.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package azure
     5  
     6  import (
     7  	stdcontext "context"
     8  	"fmt"
     9  	"strconv"
    10  
    11  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
    12  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
    13  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault"
    14  	"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
    15  	"github.com/google/uuid"
    16  	"github.com/juju/errors"
    17  
    18  	"github.com/juju/juju/cloud"
    19  	"github.com/juju/juju/environs/context"
    20  	"github.com/juju/juju/provider/azure/internal/azureauth"
    21  	"github.com/juju/juju/provider/azure/internal/errorutils"
    22  	"github.com/juju/juju/storage"
    23  )
    24  
    25  const (
    26  	// Disk encryption config attributes.
    27  	encryptedKey             = "encrypted"
    28  	diskEncryptionSetNameKey = "disk-encryption-set-name"
    29  	vaultNamePrefixKey       = "vault-name-prefix"
    30  	vaultKeyNameKey          = "vault-key-name"
    31  	vaultUserIDKey           = "vault-user-id"
    32  )
    33  
    34  // diskEncryptionInfo creates the resources needed for encrypting a disk,
    35  // including disk encryption set and vault.
    36  func (env *azureEnviron) diskEncryptionInfo(
    37  	ctx context.ProviderCallContext,
    38  	rootDisk *storage.VolumeParams,
    39  	envTags map[string]string,
    40  ) (string, error) {
    41  	if rootDisk == nil {
    42  		return "", nil
    43  	}
    44  	logger.Debugf("creating root disk encryption with parameters: %#v", *rootDisk)
    45  	// The "encrypted" value may arrive as a bool or a string.
    46  	encryptedStr, ok := rootDisk.Attributes[encryptedKey].(string)
    47  	encrypted, _ := rootDisk.Attributes[encryptedKey].(bool)
    48  	if !encrypted && ok {
    49  		encrypted, _ = strconv.ParseBool(encryptedStr)
    50  	}
    51  	if !encrypted {
    52  		logger.Debugf("encryption not enabled for root disk")
    53  		return "", nil
    54  	}
    55  
    56  	encryptionSet, _ := rootDisk.Attributes[diskEncryptionSetNameKey].(string)
    57  	vaultNamePrefix, _ := rootDisk.Attributes[vaultNamePrefixKey].(string)
    58  	keyName, _ := rootDisk.Attributes[vaultKeyNameKey].(string)
    59  	userID, _ := rootDisk.Attributes[vaultUserIDKey].(string)
    60  	if vaultNamePrefix == "" && encryptionSet == "" {
    61  		return "", errors.New("root disk encryption needs either a vault or a disk encryption set to be specified")
    62  	}
    63  
    64  	// The disk encryption set may be a reference to an existing one.
    65  	diskEncryptionSetRG, diskEncryptionSetName := referenceInfo(encryptionSet)
    66  	if diskEncryptionSetName == "" {
    67  		diskEncryptionSetName = vaultNamePrefix
    68  	}
    69  	diskEncryptionSetID := fmt.Sprintf(`[resourceId('Microsoft.Compute/diskEncryptionSets', '%s')]`, diskEncryptionSetName)
    70  	if diskEncryptionSetRG != "" {
    71  		diskEncryptionSetID = fmt.Sprintf(`[resourceId('%s', 'Microsoft.Compute/diskEncryptionSets', '%s')]`, diskEncryptionSetRG, diskEncryptionSetName)
    72  	}
    73  	// Do we just have a disk encryption set specified and no vault?
    74  	if vaultNamePrefix == "" {
    75  		return diskEncryptionSetID, nil
    76  	}
    77  
    78  	// If we need to create the disk encryption set, it must be in the model's resource group.
    79  	if diskEncryptionSetRG != "" {
    80  		return "", errors.New("do not specify a resource group for a disk encryption set to be created")
    81  	}
    82  
    83  	envTagPtr := make(map[string]*string)
    84  	for k, v := range envTags {
    85  		envTagPtr[k] = to.Ptr(v)
    86  	}
    87  
    88  	encryptionSets, err := env.encryptionSetsClient()
    89  	if err != nil {
    90  		return "", errors.Trace(err)
    91  	}
    92  	// See if the disk encryption set already exists.
    93  	existingDes, err := encryptionSets.Get(ctx, env.resourceGroup, diskEncryptionSetName, nil)
    94  	if err != nil && !errorutils.IsNotFoundError(err) {
    95  		return "", errors.Trace(err)
    96  	}
    97  	// Record the identity of an existing disk encryption set
    98  	// so we can maintain the access policy on the vault.
    99  	var desIdentity *armcompute.EncryptionSetIdentity
   100  	if err == nil {
   101  		desIdentity = existingDes.Identity
   102  	}
   103  	// The vault name must be unique across the entire subscription.
   104  	if len(vaultNamePrefix) > 15 {
   105  		return "", errors.Errorf("vault name prefix %q too long, must be 15 characters or less", vaultNamePrefix)
   106  	}
   107  
   108  	env.mu.Lock()
   109  	defer env.mu.Unlock()
   110  
   111  	vaults, err := env.vaultsClient()
   112  	if err != nil {
   113  		return "", errors.Trace(err)
   114  	}
   115  	vaultName := fmt.Sprintf("%s-%s", vaultNamePrefix, env.config.Config.UUID()[:8])
   116  	vault, vaultParams, err := env.ensureVault(ctx, vaults, vaultName, userID, envTagPtr, desIdentity)
   117  	if err != nil {
   118  		return "", errorutils.HandleCredentialError(errors.Annotatef(err, "creating vault %q", vaultName), ctx)
   119  	}
   120  
   121  	// Create a key in the vault.
   122  	if keyName == "" {
   123  		keyName = "disk-secret"
   124  	}
   125  	keyRef, err := env.createVaultKey(ctx, *vault.Properties.VaultURI, *vault.Name, keyName)
   126  	if err != nil {
   127  		return "", errorutils.HandleCredentialError(errors.Annotatef(err, "creating vault key in %q", vaultName), ctx)
   128  	}
   129  
   130  	// We had an existing disk encryption set.
   131  	if desIdentity != nil {
   132  		return diskEncryptionSetID, nil
   133  	}
   134  
   135  	// Create the disk encryption set.
   136  	desIdentity, err = env.ensureDiskEncryptionSet(ctx, encryptionSets, diskEncryptionSetName, envTagPtr, vault.ID, keyRef)
   137  	if err != nil {
   138  		return "", errorutils.HandleCredentialError(errors.Annotatef(err, "creating disk encryption set %q", diskEncryptionSetName), ctx)
   139  	}
   140  
   141  	// Update the vault access policies to allow the disk encryption set to access the key.
   142  	vaultAccessPolicies := vaultParams.Properties.AccessPolicies
   143  	vaultAccessPolicies = append(vaultAccessPolicies, vaultAccessPolicy(desIdentity))
   144  	vaultParams.Properties.AccessPolicies = vaultAccessPolicies
   145  	poller, err := vaults.BeginCreateOrUpdate(ctx, env.resourceGroup, vaultName, *vaultParams, nil)
   146  	if err == nil {
   147  		_, err = poller.PollUntilDone(ctx, nil)
   148  	}
   149  	if err != nil {
   150  		return "", errorutils.HandleCredentialError(errors.Annotatef(err, "updating vault %q access policies ", vaultName), ctx)
   151  	}
   152  	return diskEncryptionSetID, nil
   153  }
   154  
   155  // fromStringOrNil returns a UUID parsed from the input string.
   156  // Same behavior as Parse(), but returns uuid.Nil instead of an error.
   157  func fromStringOrNil(input string) uuid.UUID {
   158  	result, err := uuid.Parse(input)
   159  	if err != nil {
   160  		return uuid.Nil
   161  	}
   162  	return result
   163  }
   164  
   165  func vaultAccessPolicy(desIdentity *armcompute.EncryptionSetIdentity) *armkeyvault.AccessPolicyEntry {
   166  	tenantID := fromStringOrNil(toValue(desIdentity.TenantID))
   167  	return &armkeyvault.AccessPolicyEntry{
   168  		TenantID: to.Ptr(tenantID.String()),
   169  		ObjectID: desIdentity.PrincipalID,
   170  		Permissions: &armkeyvault.Permissions{
   171  			Keys: to.SliceOfPtrs(armkeyvault.KeyPermissionsWrapKey, armkeyvault.KeyPermissionsUnwrapKey,
   172  				armkeyvault.KeyPermissionsList, armkeyvault.KeyPermissionsGet),
   173  		},
   174  	}
   175  }
   176  
   177  // ensureDiskEncryptionSet creates or updates a disk encryption set
   178  // to use the specified vault and key.
   179  func (env *azureEnviron) ensureDiskEncryptionSet(
   180  	ctx stdcontext.Context,
   181  	encryptionSets *armcompute.DiskEncryptionSetsClient,
   182  	encryptionSetName string,
   183  	envTags map[string]*string,
   184  	vaultID, vaultKey *string,
   185  ) (*armcompute.EncryptionSetIdentity, error) {
   186  	logger.Debugf("ensure disk encryption set %q", encryptionSetName)
   187  	poller, err := encryptionSets.BeginCreateOrUpdate(ctx, env.resourceGroup, encryptionSetName, armcompute.DiskEncryptionSet{
   188  		Location: to.Ptr(env.location),
   189  		Tags:     envTags,
   190  		Identity: &armcompute.EncryptionSetIdentity{
   191  			Type: to.Ptr(armcompute.DiskEncryptionSetIdentityTypeSystemAssigned),
   192  		},
   193  		Properties: &armcompute.EncryptionSetProperties{
   194  			ActiveKey: &armcompute.KeyForDiskEncryptionSet{
   195  				SourceVault: &armcompute.SourceVault{
   196  					ID: vaultID,
   197  				},
   198  				KeyURL: vaultKey,
   199  			},
   200  		},
   201  	}, nil)
   202  	var result armcompute.DiskEncryptionSetsClientCreateOrUpdateResponse
   203  	if err == nil {
   204  		result, err = poller.PollUntilDone(ctx, nil)
   205  	}
   206  	if err != nil {
   207  		return nil, errors.Trace(err)
   208  	}
   209  	return result.Identity, nil
   210  }
   211  
   212  // ensureVault creates a vault and adds an access policy for the
   213  // specified disk encryption set identity.
   214  func (env *azureEnviron) ensureVault(
   215  	ctx stdcontext.Context,
   216  	vaults *armkeyvault.VaultsClient,
   217  	vaultName string,
   218  	userID string,
   219  	envTags map[string]*string,
   220  	desIdentity *armcompute.EncryptionSetIdentity,
   221  ) (*armkeyvault.Vault, *armkeyvault.VaultCreateOrUpdateParameters, error) {
   222  	logger.Debugf("ensure vault key %q", vaultName)
   223  	vaultTenantID := fromStringOrNil(env.tenantId)
   224  	// Create the vault with full access for the tenant.
   225  	allKeyPermissions := armkeyvault.PossibleKeyPermissionsValues()
   226  
   227  	credAttrs := env.cloud.Credential.Attributes()
   228  	appObjectID := credAttrs[credAttrApplicationObjectId]
   229  	// Older credentials don't have the application object id set,
   230  	// so look it up here and record it for next time.
   231  	if appObjectID == "" {
   232  		appID := credAttrs[credAttrAppId]
   233  		var err error
   234  		appObjectID, err = azureauth.MaybeJujuApplicationObjectID(appID)
   235  		if err != nil {
   236  			return nil, nil, errors.Annotatef(err, "credential missing %s for %q", credAttrApplicationObjectId, appID)
   237  		}
   238  		credAttrs[credAttrApplicationObjectId] = appObjectID
   239  		cred := cloud.NewCredential(env.cloud.Credential.AuthType(), credAttrs)
   240  		env.cloud.Credential = &cred
   241  	}
   242  
   243  	vaultAccessPolicies := []*armkeyvault.AccessPolicyEntry{{
   244  		TenantID: to.Ptr(vaultTenantID.String()),
   245  		ObjectID: to.Ptr(appObjectID),
   246  		Permissions: &armkeyvault.Permissions{
   247  			Keys: to.SliceOfPtrs(allKeyPermissions...),
   248  		},
   249  	}}
   250  	if userID != "" {
   251  		vaultAccessPolicies = append(vaultAccessPolicies, &armkeyvault.AccessPolicyEntry{
   252  			TenantID: to.Ptr(vaultTenantID.String()),
   253  			ObjectID: to.Ptr(userID),
   254  			Permissions: &armkeyvault.Permissions{
   255  				Keys: to.SliceOfPtrs(allKeyPermissions...),
   256  			},
   257  		})
   258  	}
   259  	if desIdentity != nil {
   260  		vaultAccessPolicies = append(vaultAccessPolicies, vaultAccessPolicy(desIdentity))
   261  	}
   262  	vaultParams := armkeyvault.VaultCreateOrUpdateParameters{
   263  		Location: to.Ptr(env.location),
   264  		Tags:     envTags,
   265  		Properties: &armkeyvault.VaultProperties{
   266  			TenantID:                 to.Ptr(vaultTenantID.String()),
   267  			EnabledForDiskEncryption: to.Ptr(true),
   268  			EnableSoftDelete:         to.Ptr(true),
   269  			EnablePurgeProtection:    to.Ptr(true),
   270  			CreateMode:               to.Ptr(armkeyvault.CreateModeDefault),
   271  			NetworkACLs: &armkeyvault.NetworkRuleSet{
   272  				Bypass:        to.Ptr(armkeyvault.NetworkRuleBypassOptionsAzureServices),
   273  				DefaultAction: to.Ptr(armkeyvault.NetworkRuleActionAllow),
   274  			},
   275  			SKU: &armkeyvault.SKU{
   276  				Family: to.Ptr(armkeyvault.SKUFamilyA),
   277  				Name:   to.Ptr(armkeyvault.SKUNameStandard),
   278  			},
   279  			AccessPolicies: vaultAccessPolicies,
   280  		},
   281  	}
   282  
   283  	// Before creating check to see if the key vault has been soft deleted.
   284  	_, err := vaults.GetDeleted(ctx, vaultName, env.location, nil)
   285  	if err != nil {
   286  		if !errorutils.IsNotFoundError(err) && !errorutils.IsForbiddenError(err) {
   287  			return nil, nil, errors.Annotatef(err, "checking for an existing soft deleted vault %q", vaultName)
   288  		}
   289  	}
   290  	if !errorutils.IsNotFoundError(err) && !errorutils.IsForbiddenError(err) {
   291  		logger.Debugf("key vault %q has been soft deleted", vaultName)
   292  		vaultParams.Properties.CreateMode = to.Ptr(armkeyvault.CreateModeRecover)
   293  	}
   294  	var result armkeyvault.VaultsClientCreateOrUpdateResponse
   295  	poller, err := vaults.BeginCreateOrUpdate(ctx, env.resourceGroup, vaultName, vaultParams, nil)
   296  	if err == nil {
   297  		result, err = poller.PollUntilDone(ctx, nil)
   298  	}
   299  	if err != nil {
   300  		return nil, nil, errors.Annotatef(err, "creating vault")
   301  	}
   302  	return &result.Vault, &vaultParams, nil
   303  }
   304  
   305  func (env *azureEnviron) deleteVault(ctx context.ProviderCallContext, vaultName string) error {
   306  	logger.Debugf("delete vault key %q", vaultName)
   307  	vaults, err := env.vaultsClient()
   308  	if err != nil {
   309  		return errors.Trace(err)
   310  	}
   311  	_, err = vaults.Delete(ctx, env.resourceGroup, vaultName, nil)
   312  	if err != nil {
   313  		err = errorutils.HandleCredentialError(err, ctx)
   314  		if !errorutils.IsNotFoundError(err) {
   315  			return errors.Annotatef(err, "deleting vault key %q", vaultName)
   316  		}
   317  	}
   318  	return nil
   319  }
   320  
   321  // createVaultKey creates, or recovers a soft deleted key,
   322  // in the specified vault.
   323  func (env *azureEnviron) createVaultKey(
   324  	ctx stdcontext.Context,
   325  	vaultBaseURI string,
   326  	vaultName string,
   327  	keyName string,
   328  ) (*string, error) {
   329  	logger.Debugf("create vault key %q in %q", keyName, vaultName)
   330  	keyClient, err := azkeys.NewClient(vaultBaseURI, env.credential, &azkeys.ClientOptions{
   331  		ClientOptions: env.clientOptions})
   332  	if err != nil {
   333  		return nil, errors.Annotatef(err, "creating vault key client for %q", vaultName)
   334  	}
   335  
   336  	resp, err := keyClient.CreateKey(
   337  		ctx,
   338  		keyName,
   339  		azkeys.CreateKeyParameters{
   340  			Kty: to.Ptr(azkeys.KeyTypeRSA),
   341  			// TODO(wallyworld) - make these configurable via storage pool attributes
   342  			KeySize: to.Ptr(int32(4096)),
   343  			KeyOps: []*azkeys.KeyOperation{
   344  				to.Ptr(azkeys.KeyOperationWrapKey),
   345  				to.Ptr(azkeys.KeyOperationUnwrapKey),
   346  			},
   347  			KeyAttributes: &azkeys.KeyAttributes{
   348  				Enabled: to.Ptr(true),
   349  			},
   350  		},
   351  		nil)
   352  	if err == nil {
   353  		return to.Ptr(string(toValue(resp.Key.KID))), nil
   354  	}
   355  	if !errorutils.IsConflictError(err) {
   356  		return nil, errors.Trace(err)
   357  	}
   358  
   359  	// If the key was previously soft deleted, recover it.
   360  	result, err := keyClient.RecoverDeletedKey(ctx, keyName, nil)
   361  	if err != nil {
   362  		return nil, errors.Annotatef(err, "restoring soft deleted vault key %q in %q", keyName, vaultName)
   363  	}
   364  	return to.Ptr(string(toValue(result.Key.KID))), nil
   365  }