Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AVM Question/Feedback]: How do we handle resources that emit secrets? #1934

Closed
1 task done
Agazoth opened this issue May 15, 2024 · 19 comments
Closed
1 task done

[AVM Question/Feedback]: How do we handle resources that emit secrets? #1934

Agazoth opened this issue May 15, 2024 · 19 comments
Labels
Needs: Attention 👋 Reply has been added to issue, maintainer to review Status: Help Wanted 🆘 Extra attention is needed Type: AVM 🅰️ ✌️ Ⓜ️ This is an AVM related issue Type: Question/Feedback 🙋 Further information is requested or just some feedback

Comments

@Agazoth
Copy link
Contributor

Agazoth commented May 15, 2024

Check for previous/existing GitHub issues

  • I have checked for previous/existing GitHub issues

Description

Building bicep modules is an exercise in making the module as slim and close to the desired resource as possible while still keeping it secure and compatible with larger deployments.

A SQL Server should not deploy a virtual network, but it might need some network resources to add it to a subnet. Likewise, we want our web site module to configure monitoring, without deploying the Insights resources.

Currently, the pattern (ptn) section of the repo is quite slim. We have some authorization stuff, some policy stuff and some security center stuff. According to the Module Classifications, a Pattern Module deploys multiple modules, usually in in alignment with the patterns in Azure Architect Center - that makes it safe to say, that we do not really have any pattern modules yet.

A Pattern module would always consist of several resources, that are deployed together in one or more resource groups distributed over one or more subscriptions. One might say, that any IaC deployment author is developing a pattern deployment for the specific project she is working on.

Authoring these patterns should be easy and secure and leverage the built-in benefits of the given language, be it bicep, terraform or others. If we need to attach a new cloud native application to an existing CAF Enterprise Scale environment, we need an easy way to add the application to the network, implement the required logging and store the secrets from the deployment in the appointed key vault.

Resources have some common requirements or behaviors that we need to address in a common way in order to keep the AVM slim and attractive. These are the things we always need to consider:

  • Network
  • Monitoring and logging
  • Authorization
  • Secrets

We often handle these requirements with the "existing" resource inside the module structure to reference the required endpoint for the integration of the specific resource we build in the module for. A quick count in the current repo reveals 358 "existing =" strings in 256 files. 32 results in 29 files are for existing key vault resources, 47 results in 46 files are for network resources and 14 results in 14 files are for insights.

The point of this specific discussion is, how we find a common way to handle resources, that emit secrets. These secrets are often used in other modules and will be visible in the deployment log, if they are added to the output of the creating module.

Ideally modules should be able to emit @secret values or directly available in the module output, but that is not the case now and it does not seem to be solved in any sprint anytime soon.

If we want to make a secret emitting resource safe and secure for the deployment builder, we have 2 options:

  1. Instruct the end users to create sub-deployments in their deployments, that handle the secrets like in this issue: Accessing Storage Account Access Key
  2. Add an optional option for storing secrets in a key vault like in the Document DB solution

I would love to hear pros and cons for different solutions. Alternative ways can also be discussed. I hope this discussion can help us find common grounds to make well structured and secure modules that deployment developers like to use.

@Agazoth Agazoth added Needs: Triage 🔍 Maintainers need to triage still Type: AVM 🅰️ ✌️ Ⓜ️ This is an AVM related issue Type: Question/Feedback 🙋 Further information is requested or just some feedback labels May 15, 2024
@eriqua
Copy link
Contributor

eriqua commented May 15, 2024

Thanks a lot @Agazoth for opening and elaborating on this issue.

@ChrisSidebotham @jtracey93 @AlexanderSehr FYI this started from the 2 PRs #1920 #1932

@Agazoth
Copy link
Contributor Author

Agazoth commented May 16, 2024

Hi @ilhaan
Happy to have a senior engineer from security on the thread 💪. I hope you will find time to share your angle on the matter.

@AlexanderSehr
Copy link
Contributor

AlexanderSehr commented May 19, 2024

Hey @Agazoth, thanks for opening the issue. We'll also discuss the topic with the other maintainers to define a general guideline.

Also, as you mentioned the output of secrets, I'll tag the corresponding Bicep issue here: Azure/bicep#2163 . It doesn't sound like there'll be the feature we're hoping for BUT a depends on for existing resources which makes a previously discussed workaround a lot easier. But it's still a workaround, so your point remains valid regardless.

PS: When it comes to the secrets alone, I'd argue that the optional 'store it in a key vault'-feature is a valid approach. So much so, that I'd argue that it would not need require a pattern as it is essentially just optionally storing some data - which happens to be a child-resource of key vault. Yet it remains very strongly correlated and in my mind does not imply a 'workload' that Patterns are 'usually' targeted towards. To be fair - the currently existing patterns are not really a good example, but that has different reasons. 😄

@microsoft-github-policy-service microsoft-github-policy-service bot added the Status: Response Overdue 🚩 When an issue/PR has not been responded to for X amount of days label May 24, 2024
@ChrisSidebotham ChrisSidebotham added Needs: Author Feedback 👂 Awaiting feedback from the issue/PR author and removed Needs: Triage 🔍 Maintainers need to triage still Status: Response Overdue 🚩 When an issue/PR has not been responded to for X amount of days labels May 24, 2024
@eriqua
Copy link
Contributor

eriqua commented May 24, 2024

Off the top of my head we have 3 options

  • OPT1: integrate the kv secrets type in the resource module. This requires a reference implementation, possibly with a spec, interface, being consistent across all modules emitting secrets.
  • OPT2: cover the secret handling scenario in a pattern module. Possibly one pattern for each resource emitting secrets. A pattern module only makes sense if we think of this scenario being reusable.
  • OPT3 example usage. In this case no module would be published covering this scenario. Instead, we may add an e2e deployment test (e.g. "with secret backup") in the corresponding resource module, leveraging the module and the Key Vault module to show how to backup secrets in a secure way

@microsoft-github-policy-service microsoft-github-policy-service bot added Needs: Attention 👋 Reply has been added to issue, maintainer to review and removed Needs: Author Feedback 👂 Awaiting feedback from the issue/PR author labels May 24, 2024
@eriqua eriqua added Needs: Author Feedback 👂 Awaiting feedback from the issue/PR author Status: Help Wanted 🆘 Extra attention is needed and removed Needs: Attention 👋 Reply has been added to issue, maintainer to review labels May 24, 2024
@Agazoth
Copy link
Contributor Author

Agazoth commented May 25, 2024

Hey @eriqua - happy for your insights.

Option 2 would possibly mean, that we would have to create a pattern for each resource that emit secrets alongside a module that does not emit the secrets for each of those resources.

The secrets generated at deployment time are almost always used elsewhere in a deployment, which makes the modules practically useless.

Option 3 would mean, that we write the "workaround" sample into each resource module and it is the still up to the user to write the extra files for their deployment.

I agree with all your points in option 1. Option 1 is also the solution I would advocate for:

  • It makes the module relevant as opposed to option 2.
  • It implements a save, Azure native access to the secrets emitted
  • It builds the "workaround" into the module making template authoring simpler and safer, thus surpasses option 3.

All 3 options will leave us with work, that needs to be done:

Option 1:

  • Find a reference implementation (as stated, the one in document-db works like a charm)
  • Define specs and interfaces

Option 2:

  • If we want to use patterns for managing emitted secrets, we need to start working on patterns in general to clarify, how they are intendet to be used. At least for me, that is unclear, if not as described - for larger implementations.
  • Define what goes in the module and what goes in the pattern for secret emitting resources - and if the modules should be built at all.

Option 3:

  • Add the workaround sample into all secret emitting modules. This in turn calls for:
    • A reference implementation that exemplifies the implementation
    • Defining specs for the e2e deployment tests

In a more general view on modules, I find great inspiration in this article from Breandan Thomsen https://brendanthompson.com/posts/2020/terraform-to-module-or-not-to-module. He is a Terraform ambassador and has been working with modules and module building for many years. He highlights these 9 points for module creation:

  1. If a resource MUST have security/audit/compliance controls enforced at provision and/or configuration time, this MUST then be encapsulated within a Terraform module. Otherwise, it SHOULD be consumed as a Terraform resource.
  2. When specific configuration or metadata MUST be applied to a resource it MUST then be created as a Terraform module to enforce those requirements.
  3. When a resource has a stringent/complex naming required that MUST be adhered to this resource MUST be created as a Terraform module.
  4. When a resource relies on information/metadata/context that cannot be known by the consumer it MUST be created as a Terraform module.
  5. If a Terraform module will reduce the complexity of either the configuration or its interface then a Terraform module SHOULD be used.
  6. Resources that have an aligned business purpose/requirement SHOULD be grouped together as a Terraform module.
  7. Resources that MUST satisfy multiple use cases SHOULD be created as a Terraform module.
  8. When a group of resources shares a similar lifecycle they SHOULD be grouped together as a Terraform module.
  9. If a resource is always going to be consumed by a higher-order module it SHOULD NOT be created as a bare/wrapper Terraform module

I see the first point as a clear indicator for option 1. Actually, it also solves the question about whether or not to create a module in option 2.

Point 5 is also a good argument for option 1. The end-user of the module gets a built-in option to safely store the secret in a place, where Azure native tooling can manage and cycle the secrets.

Point 7 would also favour option 1. The module can still be used in multiple use cases, whereas option 2 would need specific patterns for each secret emitting resource leaving out the module, much like option 3 just describes how to work around a super common use case that is not implemented in the module

Point 9 too can be used for favouring option 1. The simplest example is the storage account. The storage account is used in a very high percentage of implementations and is almost always consumed by some other resource, that needs the functionality it provides. Stretching point 9 a bit, one could say, that a web app consumes the connection string or primary key of the storage account for the storage queue or DataLakeGen2 access. This means that secret emitting resource modules SHOULD NOT be mere wrappers.

I think my opinion is quite clear. Having secrets emitted directly to key vault from the module is the right implementation until the changes on ARM allows us to emit secrets directly from the bicep resources. It gives the module users a simple, unified solution to get access to secrets from the modules, that is secure and supported by the Azure platform without having to remember to write workarounds every time a resource has a secret they want.

Modules with this implementation will also work very well in Pattern implementations, that often deploy their own key vaults. And pattern implementations that reference existing key vaults for that matter.

I'm looking forward to other viewpoints or angles and very eager to go to work on this - I have a private registry to dissolve 😸

@microsoft-github-policy-service microsoft-github-policy-service bot added Needs: Attention 👋 Reply has been added to issue, maintainer to review and removed Needs: Author Feedback 👂 Awaiting feedback from the issue/PR author labels May 25, 2024
@Agazoth
Copy link
Contributor Author

Agazoth commented Jun 4, 2024

Hey @AlexanderSehr and @eriqua did you manage to find anyone with additional opinions or ideas on how to handle secrets?

@AlexanderSehr
Copy link
Contributor

AlexanderSehr commented Jun 7, 2024

Hey @Agazoth,
that was quite a read. Thank you for that 💪 We did start debating it among the maintainers last week, but did not yet agree on the specific approach. Once we do, we'll let you know asap. And if we end up with option 1, for example, try to define a reasonable spec asap. It's unfortunately only one of many topics, so we have to do as much as we can with the capacity we have.

For option 1,for example, I'd argue the interface/spec should ensure

  • that a user can set names, but also have a default
  • that a user must provide a key vault resource ID
  • that the feature must be optional
    • Setting individual secrets should be optional (as opposed to all or nothing)
  • that the interface must have a user defined type
  • that the template must be implemented in a /modules/ subfolder (which is the convention for any helper templates)
  • ...

Personally I think this option makes the most sense. I'd vote for OPT2 if it would be something big, but as it is (in this case) only a rather small extra deployment that just needs one parameter, I may just be simple enough to be part of the module itself. Aside the fact that one could argue it is strongly correlated.

Option 3 aligns with what I described in an earlier conversation, that is, how I usually work around the limitation of Bicep when building solutions that needs resource data (like a storage account key). And while this sentiment did not change, I don't think an example test in AVM would make sense. If we were to add one, it would only be sitting in the test file, and not be pulled into the readme. In other words - you'd need to know where it is to find it and then try and make something out of it. It may just be too complicated.

@AlexanderSehr
Copy link
Contributor

AlexanderSehr commented Jun 9, 2024

I had a bit of time so I did some trial & error how the interface could look like and currently ended up with the following proposal (using the CosmosDB solution as a use case):

keyVaultExport.bicep

Based on the solution of @bryansan-msft

@description('Required. The name of the key vault to set the secrets in.')
param keyVaultName string

@description('Required. The secrets to set in the key vault.')
param keySecrets keySecret[]

resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
  name: keyVaultName
}

resource keySecretsSecrets 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = [
  for secret in keySecrets: {
    name: secret.name
    parent: keyVault
    properties: {
      value: secret.value
    }
  }
]

type keySecret = {
  @description('Required. The name of the secret to set.')
  name: string

  @description('Required. The value of the secret to set.')
  @secure()
  value: string
}

main.bicep

@description('Optional. Key vault reference and secret settings for the module''s secrets export.')
param secretsExportConfiguration secretsExportConfigurationType?

module keyVaultTest 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
  name: '${uniqueString(deployment().name, location)}-secrets-kv'
  scope: resourceGroup(
    split(secretsExportConfiguration!.keyVaultResourceId, '/')[2],
    split(secretsExportConfiguration!.keyVaultResourceId, '/')[4]
  )
  params: {
    keyVaultName: last(split(secretsExportConfiguration!.keyVaultResourceId, '/'))
    keySecrets: union(
      [],
      secretsExportConfiguration.?setPrimaryWriteKey != false
        ? [
            {
              name: secretsExportConfiguration.?primaryWriteKeySecretName ?? 'Primary-Write-Key'
              value: databaseAccount.listKeys().primaryMasterKey
            }
          ]
        : [],
      secretsExportConfiguration.?setPrimaryReadOnlyKey != false
        ? [
            {
              name: secretsExportConfiguration.?primaryReadOnlyKeySecretName ?? 'Primary-Readonly-Key'
              value: databaseAccount.listKeys().primaryReadonlyMasterKey
            }
          ]
        : []
        // (...)
    )
  }
}

type secretsExportConfigurationType = {
  @description('Required. The resource ID of the key vault where to store the secrets of this module.')
  keyVaultResourceId: string

  @description('Optional. The name for secret to create.')
  primaryWriteKeySecretName: string?
  @description('Optional. Set to control wether or not to create the secret. Defaults to true.')
  setPrimaryWriteKey: bool?
  
  @description('Optional. The name for secret to create.')
  primaryReadOnlyKeySecretName: string?
  @description('Optional. Set to control wether or not to create the secret. Defaults to true.')
  setPrimaryReadOnlyKey: bool?

  // (...)
}

main.test.bicep

module testDeployment '../../../main.bicep' = [
  for iteration in ['init', 'idem']: {
    scope: resourceGroup
    name: '${uniqueString(deployment().name, enforcedLocation)}-test-${serviceShort}-${iteration}'
    params: {
     secretsExportConfiguration: {
        keyVaultResourceId: keyVaultResourceId
        primaryReadOnlyKeySecretName: 'myName'
        setPrimaryWriteConnectionString: false
      }
     // (...)
}]

I did play around with a couple different variants like

  • Having one object per secret with an optional name & enabled property: Turned out to be rather confusing to use if you don't ask the user to specify all secret 'objects' to set. However, as we want to set all by default it got a bit messy.
  • Having an array secretsToSet and then one optional parameter for each name: Also this one was a bit confusion as you need to handle cases like 'somebody set the name, but did not configure the value in the secretsToSet property. And again, how to set all by default etc.
  • Moving the 'pulling' of secrets from the main.bicep into the nested file to move the complexity: This would only work if the Key Vault is definitely in the same Resource Group as in this case the CosmosDB. Not a good restriction to have.

Nothing is yet decided, but I wanted to give it a shot anyways. Happy to hear some thoughts.

@Agazoth
Copy link
Contributor Author

Agazoth commented Jun 11, 2024

Hi @AlexanderSehr

I also found a bit of time to play around with different solutions, and I do agree with all your findings.

The precondition in CosmosDB is, that we want to save ALL secrets but that might not really be the case. Currently it sets a predefined name for all the secrets if a secret name is not specified. In the real world, we often only need one or 2 specified secrets.

If we change the logic so only defined secrets are exported to the key vault we can keep the optional string for the secret name and let that trigger the secret deployment, thus avoiding the enabled property.

That would look something like this:

keyVaultExport.bicep

@description('Required. The name of the key vault to set the secrets in.')
param keyVaultName string

@description('Required. The secrets to set in the key vault.')
param keySecrets keySecret[]

resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
  name: keyVaultName
}

resource keySecretsSecrets 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = [
  for secret in keySecrets: if (secret.name != 'DONOTDEPLOY') {
    name: secret.name!
    parent: keyVault
    properties: {
      value: secret.value
    }
  }
]

type keySecret = {
  @description('Name of the secret. If omitted, the secret is not set.')
  name: string?

  @description('Required. The value of the secret to set.')
  @secure()
  value: string
}

Then we can simplify main:

main.bicep

@description('Optional. Key vault reference and secret settings for the module''s secrets export.')
param secretsExportConfiguration secretsExportConfigurationType?

module keyVaultTest 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
  name: '${uniqueString(deployment().name, location)}-secrets-kv'
  scope: resourceGroup(
    split(secretsExportConfiguration!.keyVaultResourceId, '/')[2],
    split(secretsExportConfiguration!.keyVaultResourceId, '/')[4]
  )
  params: {
    keyVaultName: last(split(secretsExportConfiguration!.keyVaultResourceId, '/'))
    keySecrets: [
          {
            name: secretsExportConfiguration.?primaryWriteKeySecretName ?? 'DONOTDEPLOY'
            value: databaseAccount.listKeys().primaryMasterKey
          }
          {
              name: secretsExportConfiguration.?primaryReadOnlyKeySecretName ?? 'DONOTDEPLOY'
              value: databaseAccount.listKeys().primaryReadonlyMasterKey
          }
        // (...)
    ]
}

type secretsExportConfigurationType = {
  @description('Required. The resource ID of the key vault where to store the secrets of this module.')
  keyVaultResourceId: string

  @description('Optional. The name for secret to create.')
  primaryWriteKeySecretName: string?

  
  @description('Optional. The name for secret to create.')
  primaryReadOnlyKeySecretName: string?


  // (...)
}

Then running a deployment with the module would look a bit like this if key vault is in another rg

main.test.bicep

targetScope = 'subscription'

resource resourcerg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
  name: 'rg-resource'
  location: 'westeurope'
}

resource kvrg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
  name: 'rg-keyvault'
  location: 'westeurope'
}

module kv 'br/public:avm/res/key-vault/vault:0.6.1' = {
  name: 'Deploy-KeyVault'
  scope: kvrg
  params: {
    name: 'kv-demosecret'
    location: kvrg.location
    enableVaultForDeployment: true
    enableSoftDelete: false
    enablePurgeProtection: false
  }
}


module res 'main.bicep' = {
  name: 'Deploy-With-Key-Vault'
  scope: resourcerg
  params: {
    secretsExportConfiguration: {
      keyVaultId: kv.outputs.resourceId
      primaryKeySecretName: 'PrimaryKeyDefined'
      secondaryKeySecretName: 'ThisWorksToo'
    }
  }
}

This way we can cover all secrets emitted by a resource with an option to place it in a key vault. The key vault can be pre-existing, given that the deployer has access, or be deployed in the same template.

The 'DONOTDEPLOY' bit is not too pretty, but it is easier to read then the union array which i could not get to work with an optional name.

I think we need to take a basic decision on, whether all secrets should be emitted to key vault or only defined secrets should go there.

  • If all secrets are pushed, the generic names might overwrite each other if multiple instances of the same resource is deployed and secrets are stored.
  • Just exporting user defined secrets gives the template author more control of the deployment.

Edit by @AlexanderSehr: added bicep to code blocks to enable syntax highlighting

@AlexanderSehr
Copy link
Contributor

Hey @Agazoth, just as a quick update to you: After some firefighting, I just put this item back on the agenda. I hope we can come to an agreement this Thursday, or the next. Fingers crossed. Please excuse that it takes longer than we would have hoped.

@Agazoth
Copy link
Contributor Author

Agazoth commented Jul 9, 2024

That is great news @AlexanderSehr - I'm looking forward to it 🙂. Thanks for your continued effort.

@AlexanderSehr
Copy link
Contributor

AlexanderSehr commented Jul 11, 2024

After discussing with both @eriqua & @ReneHezser, this is another proposal we agreed on that requires the user to opt in for every secret instead of storing all secrets by default. Also, the resource IDs of every secret created is returned which should be helpful to reference them again later.
This, and the other proposals are still to get a final sign-off by ideally next week.

keyVaultExport.bicep

Based on the solution of @bryansan-msft

@description('Required. The name of the Key Vault to set the ecrets in.')
param keyVaultName string

@description('Required. The secrets to set in the Key Vault.')
param secretsToSet secretToSetType[]

resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
  name: keyVaultName
}

resource secrets 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = [
  for secret in secretsToSet: {
    name: secret.name
    parent: keyVault
    properties: {
      value: secret.value
    }
  }
]

@description('The references to the secrets exported to the provided Key Vault.')
output secretsSet secretSetType[] = [
  #disable-next-line outputs-should-not-contain-secrets // Only returning the references, not a secret value
  for index in range(0, length(secretsToSet ?? [])): {
    secretResourceId: secrets[index].id
    secretUri: secrets[index].properties.secretUri
  }
]

// =============== //
//   Definitions   //
// =============== //

@export()
type secretSetType = {
  @description('The resourceId of the exported secret.')
  secretResourceId: string

  @description('The secret URI of the exported secret.')
  secretUri: string
}

type secretToSetType = {
  @description('Required. The name of the secret to set.')
  name: string

  @description('Required. The value of the secret to set.')
  @secure()
  value: string
}

main.bicep

@description('Optional. Key vault reference and secret settings for the module\'s secrets export.')
param secretsExportConfiguration secretsExportConfigurationType?

module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
  name: '${uniqueString(deployment().name, location)}-secrets-kv'
  scope: resourceGroup(
    split((secretsExportConfiguration.?keyVaultResourceId ?? '//'), '/')[2],
    split((secretsExportConfiguration.?keyVaultResourceId ?? '////'), '/')[4]
  )
  params: {
    keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId ?? '//', '/'))
    secretsToSet: union(
      [],
      contains(secretsExportConfiguration!, 'primaryWriteKeySecretName')
        ? [
            {
              name: secretsExportConfiguration!.primaryWriteKeySecretName
              value: databaseAccount.listKeys().primaryMasterKey
            }
          ]
        : [],
      contains(secretsExportConfiguration!, 'primaryReadOnlyKeySecretName')
        ? [
            {
              name: secretsExportConfiguration!.primaryReadOnlyKeySecretName
              value: databaseAccount.listKeys().primaryReadonlyMasterKey
            }
          ]
        : []
        // (...)
    )
  }
}

@description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.')
output exportedSecrets secretsOutputType = (secretsExportConfiguration != null)
  ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret)
  : {}

// =============== //
//   Definitions   //
// =============== //

type secretsExportConfigurationType = {
  @description('Required. The resource ID of the key vault where to store the secrets of this module.')
  keyVaultResourceId: string

  @description('Optional. The primary write key secret name to create.')
  primaryWriteKeySecretName: string?

  @description('Optional. The primary readonly key secret name to create.')
  primaryReadOnlyKeySecretName: string?

  // (...)
}

import { secretSetType } from 'modules/keyVaultExport.bicep'
type secretsOutputType = {
  @description('An exported secret\'s references.')
  *: secretSetType
}

main.test.bicep

module testDeployment '../../../main.bicep' = [
  for iteration in ['init', 'idem']: {
    scope: resourceGroup
    name: '${uniqueString(deployment().name, enforcedLocation)}-test-${serviceShort}-${iteration}'
    params: {
     secretsExportConfiguration: {
        keyVaultResourceId: keyVaultResourceId
        primaryReadOnlyKeySecretName: 'myPrimarySecret'
        primaryWriteConnectionStringName: 'mySecondarySecret'
      }
     // (...)
}]

With the output being a hashtable with the secret's name as the key, you could then reference the secret properties ID & URL via the name of the secret. For example

// Used an output in the test file to return the result as shown below
output exportedSecrets object = testDeployment.outputs.exportedSecrets
output specificSecret string = testDeployment.outputs.exportedSecrets.myPrimarySecret.secretResourceId
output exportedSecretResourceIds array = map(
  items(testDeployment.outputs.exportedSecrets),
  item => item.value.secretResourceId
)

Which results in an output like

{
  "exportedSecrets": {
    "Type": "Object",
    "Value": {
      "myPrimarySecret": {
        "secretResourceId": "/subscriptions/<subId>/resourceGroups/dep-avm-documentdb.databaseaccounts-dddamin-rg/providers/Microsoft.KeyVault/vaults/dep-avm-kvlt-dddamin/secrets/myPrimarySecret",   
        "secretUri": "https://dep-avm-kvlt-dddamin.vault.azure.net/secrets/myPrimarySecret"
      },
      "mySecondarySecret": {
        "secretResourceId": "/subscriptions/<subId>/resourceGroups/dep-avm-documentdb.databaseaccounts-dddamin-rg/providers/Microsoft.KeyVault/vaults/dep-avm-kvlt-dddamin/secrets/mySecondarySecret",
        "secretUri": "https://dep-avm-kvlt-dddamin.vault.azure.net/secrets/mySecondarySecret"
      }
    }
  },
  "specificSecret": {
    "Type": "String",
    "Value": "/subscriptions/<subId>/resourceGroups/dep-avm-documentdb.databaseaccounts-dddamin-rg/providers/Microsoft.KeyVault/vaults/dep-avm-kvlt-dddamin/secrets/myPrimarySecret"
  },
  "exportedSecretResourceIds": {
    "Type": "Array",
    "Value": [
      "/subscriptions/<subId>/resourceGroups/dep-avm-documentdb.databaseaccounts-dddamin-rg/providers/Microsoft.KeyVault/vaults/dep-avm-kvlt-dddamin/secrets/mySecondarySecret",
      "/subscriptions/<subId>/resourceGroups/dep-avm-documentdb.databaseaccounts-dddamin-rg/providers/Microsoft.KeyVault/vaults/dep-avm-kvlt-dddamin/secrets/myPrimarySecret"
    ]
  }
}

@AlexanderSehr
Copy link
Contributor

AlexanderSehr commented Aug 1, 2024

Hey @Agazoth,
we agreed on the last iteration suggested above - finally 😄
That is

  • The person deploying needs to 'opt-in' to set a secret (i.e., secrets are not set by default) by providing a name
  • The implementation returns a hashtable of the configured secrets through which the person that sets the secret could directly get access to the secret's resourceID & url, if needed
  • If a module owner adds the feature, they should use the suggested implementation 1:1 - aside from which secrets are set (as it's different from resource type to resource type) which will have an affect on the keyVaultExport.bicep module deployment in the main.bicep & the UDT

I'll go ahead and define a spec for that to document it on AVM. In the meantime, if you find time, please update your PRs to the above implementation and we can get them right in.

If you have any last-minute feedback, please let me know 💪

@Agazoth
Copy link
Contributor Author

Agazoth commented Aug 1, 2024

Hi @AlexanderSehr

That is excellent news and a really good decision! I'll update the PRs accordingly as soon as I'm back from vacation (next week).

Thank you for your persistence!

@AlexanderSehr
Copy link
Contributor

Hi @AlexanderSehr

That is excellent news and a really good decision! I'll update the PRs accordingly as soon as I'm back from vacation (next week).

Thank you for your persistence!

Thank you for your partience. I'm sure we'll catch up soon. Enjoy your vacation, while you can 💪

AlexanderSehr added a commit that referenced this issue Aug 5, 2024
## Description

- Updated current secrets export interface to spec as per
[ref](#1934 (comment))
- Updated to latest RBAC schema (cc: @krbar)
- Addressed warnings

## Pipeline Reference

<!-- Insert your Pipeline Status Badge below -->

| Pipeline |
| -------- |
|
[![avm.res.document-db.database-account](https://github.com/Azure/bicep-registry-modules/actions/workflows/avm.res.document-db.database-account.yml/badge.svg?branch=users%2Falsehr%2FemittedSecretsTest&event=workflow_dispatch)](https://github.com/Azure/bicep-registry-modules/actions/workflows/avm.res.document-db.database-account.yml)
|

## Type of Change

<!-- Use the checkboxes [x] on the options that are relevant. -->

- [ ] Update to CI Environment or utilities (Non-module affecting
changes)
- [x] Azure Verified Module updates:
- [ ] Bugfix containing backwards-compatible bug fixes, and I have NOT
bumped the MAJOR or MINOR version in `version.json`:
- [ ] Someone has opened a bug report issue, and I have included "Closes
#{bug_report_issue_number}" in the PR description.
- [ ] The bug was found by the module author, and no one has opened an
issue to report it yet.
- [ ] Feature update backwards compatible feature updates, and I have
bumped the MINOR version in `version.json`.
- [ ] Breaking changes and I have bumped the MAJOR version in
`version.json`.
  - [ ] Update to documentation

## Checklist

- [ ] I'm sure there are no other open Pull Requests for the same
update/change
- [ ] I have run `Set-AVMModule` locally to generate the supporting
module files.
- [ ] My corresponding pipelines / checks run clean and green without
any errors or warnings

<!-- Please keep up to date with the contribution guide at
https://aka.ms/avm/contribute/bicep -->
@eriqua
Copy link
Contributor

eriqua commented Sep 27, 2024

Hey @Agazoth, @AlexanderSehr as specs are defined and the first PRs implementing those are merged, shall we close this issue?

@Agazoth
Copy link
Contributor Author

Agazoth commented Sep 27, 2024

@eriqua that is fine with me. So glad this got implemented. Already using it in deployments and it works like a charm 😃

@eriqua
Copy link
Contributor

eriqua commented Sep 27, 2024

Very glad to hear @Agazoth, thanks for triggering this conversation, that's only how this feature got implemented 😉

Will wait for Alex to confirm as well and then close.

@AlexanderSehr
Copy link
Contributor

Sounds good. Thanks 💪

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs: Attention 👋 Reply has been added to issue, maintainer to review Status: Help Wanted 🆘 Extra attention is needed Type: AVM 🅰️ ✌️ Ⓜ️ This is an AVM related issue Type: Question/Feedback 🙋 Further information is requested or just some feedback
Projects
Development

No branches or pull requests

4 participants