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 }