Skip to content

Commit b844489

Browse files
feat: Add support to store ElasticSearch service credentials in Secrets Manager (#247)
* add service credential to secrets manager in DA * tests: skipping tests other than TestRunStandardSolutionSchematics * fix: update secret_manager_service_credential * feat support to store service credentials in secrets manager * update test * test failure work around * fix: pre-commit failure * add condition to not create sm credentials * added back connectionstrings lifecycle ignore * fix test failure * update description * fix: resolve review comments * fix: add validation --------- Co-authored-by: Soaib024 <[email protected]> Co-authored-by: Soaib024 <[email protected]>
1 parent 47e831a commit b844489

File tree

6 files changed

+235
-8
lines changed

6 files changed

+235
-8
lines changed

.secrets.baseline

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "go.sum|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2024-07-10T14:07:34Z",
6+
"generated_at": "2024-08-07T07:25:45Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -92,15 +92,23 @@
9292
"hashed_secret": "44cdfc3615970ada14420caaaa5c5745fca06002",
9393
"is_secret": false,
9494
"is_verified": false,
95-
"line_number": 58,
95+
"line_number": 59,
9696
"type": "Secret Keyword",
9797
"verified_result": null
9898
},
9999
{
100100
"hashed_secret": "bd0d0d73a240c29656fb8ae0dfa5f863077788dc",
101101
"is_secret": false,
102102
"is_verified": false,
103-
"line_number": 63,
103+
"line_number": 64,
104+
"type": "Secret Keyword",
105+
"verified_result": null
106+
},
107+
{
108+
"hashed_secret": "1e5c2f367f02e47a8c160cda1cd9d91decbac441",
109+
"is_secret": false,
110+
"is_verified": false,
111+
"line_number": 192,
104112
"type": "Secret Keyword",
105113
"verified_result": null
106114
}

solutions/standard/DA-types.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Several optional input variables in the IBM Cloud [Databases for Elasticsearch d
55
- [Service credentials](#svc-credential-name) (`service_credential_names`)
66
- [Users](#users) (`users`)
77
- [Autoscaling](#autoscaling) (`auto_scaling`)
8+
- [Service credential secrets](#service-credential-secrets) (`service_credential_secrets`)
89

910
## Service credentials <a name="svc-credential-name"></a>
1011

@@ -130,3 +131,74 @@ The following example shows values for both disk and memory for the `auto_scalin
130131
}
131132
}
132133
```
134+
135+
## Service credential secrets <a name="service-credential-secrets"></a>
136+
137+
When you add an IBM Databases for Elasticsearch service from the IBM Cloud catalog to an IBM Cloud Projects service, you can configure service credentials. In the edit mode for the projects configuration, select the Configure panel and then click the optional tab.
138+
139+
In the configuration, specify the secret group name, whether it already exists or will be created and include all the necessary service credential secrets that need to be created within that secret group.
140+
141+
To enter a custom value, use the edit action to open the "Edit Array" panel. Add the service credential secrets configurations to the array here.
142+
143+
[Learn more](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/data-sources/sm_service_credentials_secret) about service credential secrets.
144+
145+
- Variable name: `service_credential_secrets`.
146+
- Type: A list of objects that represent a service credential secret groups and secrets
147+
- Default value: An empty list (`[]`)
148+
149+
### Options for service_credential_secrets
150+
151+
- `secret_group_name` (required): A unique human-readable name that identifies this service credential secret group.
152+
- `secret_group_description` (optional, default = `null`): A human-readable description for this secret group.
153+
- `existing_secret_group`: (optional, default = `false`): Set to true, if secret group name provided in the variable `secret_group_name` already exists.
154+
- `service_credentials`: (optional, default = `[]`): A list of object that represents a service credential secret.
155+
156+
### Options for service_credentials
157+
158+
- `secret_name`: (required): A unique human-readable name of the secret to create.
159+
- `service_credentials_source_service_role`: (required): The role to give the service credential in the Databases for Elasticsearch service. Acceptable values are `Writer`, `Reader`, `Manager`, and `None`
160+
- `secret_labels`: (optional, default = `[]`): Labels of the secret to create. Up to 30 labels can be created. Labels can be 2 - 30 characters, including spaces. Special characters that are not permitted include the angled brackets (<>), comma (,), colon (:), ampersand (&), and vertical pipe character (|).
161+
- `secret_auto_rotation`: (optional, default = `true`): Whether to configure automatic rotation of service credential.
162+
- `secret_auto_rotation_unit`: (optional, default = `day`): Specifies the unit of time for rotation of a secret. Acceptable values are `day` or `month`.
163+
- `secret_auto_rotation_interval`: (optional, default = `89`): Specifies the rotation interval for the rotation unit.
164+
- `service_credentials_ttl`: (optional, default = `7776000`): The time-to-live (TTL) to assign to generated service credentials (in seconds).
165+
- `service_credential_secret_description`: (optional, default = `null`): Description of the secret to create.
166+
167+
The following example includes all the configuration options for four service credentials and two secret groups.
168+
```hcl
169+
[
170+
{
171+
"secret_group_name": "sg-1"
172+
"existing_secret_group": true
173+
"service_credentials": [
174+
{
175+
"secret_name": "cred-1"
176+
"service_credentials_source_service_role": "Writer"
177+
"secret_labels": ["test-writer-1", "test-writer-2"]
178+
"secret_auto_rotation": true
179+
"secret_auto_rotation_unit": "day"
180+
"secret_auto_rotation_interval": 89
181+
"service_credentials_ttl": 7776000
182+
"service_credential_secret_description": "sample description"
183+
},
184+
{
185+
"secret_name": "cred-2"
186+
"service_credentials_source_service_role": "Reader"
187+
}
188+
]
189+
},
190+
{
191+
"secret_group_name": "sg-2"
192+
"service_credentials": [
193+
{
194+
"secret_name": "cred-3"
195+
"service_credentials_source_service_role": "Editor"
196+
},
197+
{
198+
"secret_name": "cred-4"
199+
"service_credentials_source_service_role": "None"
200+
}
201+
]
202+
}
203+
]
204+
```

solutions/standard/main.tf

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ locals {
2020
kms_service_name = local.kms_key_crn != null ? (
2121
can(regex(".*kms.*", local.kms_key_crn)) ? "kms" : can(regex(".*hs-crypto.*", local.kms_key_crn)) ? "hs-crypto" : null
2222
) : null
23+
24+
elasticsearch_guid = local.use_existing_db_instance ? data.ibm_database.existing_db_instance[0].guid : module.elasticsearch[0].guid
2325
}
2426

2527
#######################################################################################################################
@@ -59,7 +61,6 @@ resource "time_sleep" "wait_for_authorization_policy" {
5961
create_duration = "30s"
6062
}
6163

62-
6364
module "kms" {
6465
providers = {
6566
ibm = ibm.kms
@@ -121,6 +122,66 @@ module "elasticsearch" {
121122
elser_model_type = var.elser_model_type
122123
}
123124

125+
# create a service authorization between Secrets Manager and the target service (Elastic Search)
126+
resource "ibm_iam_authorization_policy" "secrets_manager_key_manager" {
127+
count = var.skip_es_sm_auth_policy || var.existing_secrets_manager_instance_crn == null ? 0 : 1
128+
depends_on = [module.elasticsearch]
129+
source_service_name = "secrets-manager"
130+
source_resource_instance_id = local.existing_secrets_manager_instance_guid
131+
target_service_name = "databases-for-elasticsearch"
132+
target_resource_instance_id = local.elasticsearch_guid
133+
roles = ["Key Manager"]
134+
description = "Allow Secrets Manager with instance id ${local.existing_secrets_manager_instance_guid} to manage key for the databases-for-elasticsearch instance"
135+
}
136+
137+
# workaround for https://github.com/IBM-Cloud/terraform-provider-ibm/issues/4478
138+
resource "time_sleep" "wait_for_es_authorization_policy" {
139+
depends_on = [ibm_iam_authorization_policy.secrets_manager_key_manager]
140+
create_duration = "30s"
141+
}
142+
143+
locals {
144+
service_credential_secrets = [
145+
for service_credentials in var.service_credential_secrets : {
146+
secret_group_name = service_credentials.secret_group_name
147+
secret_group_description = service_credentials.secret_group_description
148+
existing_secret_group = service_credentials.existing_secret_group
149+
secrets = [
150+
for secret in service_credentials.service_credentials : {
151+
secret_name = secret.secret_name
152+
secret_labels = secret.secret_labels
153+
secret_auto_rotation = secret.secret_auto_rotation
154+
secret_auto_rotation_unit = secret.secret_auto_rotation_unit
155+
secret_auto_rotation_interval = secret.secret_auto_rotation_interval
156+
service_credentials_ttl = secret.service_credentials_ttl
157+
service_credential_secret_description = secret.service_credential_secret_description
158+
service_credentials_source_service_role = secret.service_credentials_source_service_role
159+
service_credentials_source_service_crn = local.use_existing_db_instance ? data.ibm_database.existing_db_instance[0].id : module.elasticsearch[0].crn
160+
secret_type = "service_credentials" #checkov:skip=CKV_SECRET_6
161+
}
162+
]
163+
}
164+
]
165+
166+
existing_secrets_manager_instance_crn_split = var.existing_secrets_manager_instance_crn != null ? split(":", var.existing_secrets_manager_instance_crn) : null
167+
existing_secrets_manager_instance_guid = var.existing_secrets_manager_instance_crn != null ? element(local.existing_secrets_manager_instance_crn_split, length(local.existing_secrets_manager_instance_crn_split) - 3) : null
168+
existing_secrets_manager_instance_region = var.existing_secrets_manager_instance_crn != null ? element(local.existing_secrets_manager_instance_crn_split, length(local.existing_secrets_manager_instance_crn_split) - 5) : null
169+
170+
# tflint-ignore: terraform_unused_declarations
171+
validate_sm_crn = length(local.service_credential_secrets) > 0 && var.existing_secrets_manager_instance_crn == null ? tobool("`existing_secrets_manager_instance_crn` is required when adding service credentials to a secrets manager secret.") : false
172+
}
173+
174+
module "secrets_manager_service_credentials" {
175+
count = length(local.service_credential_secrets) > 0 ? 1 : 0
176+
depends_on = [time_sleep.wait_for_es_authorization_policy]
177+
source = "terraform-ibm-modules/secrets-manager/ibm//modules/secrets"
178+
version = "1.17.4"
179+
existing_sm_instance_guid = local.existing_secrets_manager_instance_guid
180+
existing_sm_instance_region = local.existing_secrets_manager_instance_region
181+
endpoint_type = var.existing_secrets_manager_endpoint_type
182+
secrets = local.service_credential_secrets
183+
}
184+
124185
# this extra block is needed when passing in an existing ES instance - the database data block
125186
# requires a name and resource_id to retrieve the data
126187
data "ibm_resource_instance" "existing_instance_resource" {

solutions/standard/outputs.tf

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ output "id" {
99

1010
output "guid" {
1111
description = "Elasticsearch instance guid"
12-
value = local.use_existing_db_instance ? data.ibm_database.existing_db_instance[0].guid : module.elasticsearch[0].guid
12+
value = local.elasticsearch_guid
1313
}
1414

1515
output "version" {
@@ -48,3 +48,13 @@ output "port" {
4848
description = "Elasticsearch instance port"
4949
value = local.use_existing_db_instance ? data.ibm_database_connection.existing_connection[0].https[0].hosts[0].port : module.elasticsearch[0].port
5050
}
51+
52+
output "service_credential_secrets" {
53+
description = "Service credential secrets"
54+
value = length(local.service_credential_secrets) > 0 ? module.secrets_manager_service_credentials[0].secrets : null
55+
}
56+
57+
output "service_credential_secret_groups" {
58+
description = "Service credential secret groups"
59+
value = length(local.service_credential_secrets) > 0 ? module.secrets_manager_service_credentials[0].secret_groups : null
60+
}

solutions/standard/variables.tf

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ variable "auto_scaling" {
179179
# Encryption
180180
##############################################################
181181

182-
183182
variable "existing_kms_key_crn" {
184183
type = string
185184
description = "The CRN of a Hyper Protect Crypto Services or Key Protect root key to use for disk encryption. If not specified, a root key is created in the KMS instance."
@@ -238,6 +237,65 @@ variable "enable_elser_model" {
238237
default = false
239238
}
240239

240+
##############################################################################
241+
## Secrets Manager Service Credentials
242+
##############################################################################
243+
244+
variable "existing_secrets_manager_instance_crn" {
245+
type = string
246+
default = null
247+
description = "The CRN of existing secrets manager to use to create service credential secrets for Databases for Elasticsearch instance."
248+
}
249+
250+
variable "existing_secrets_manager_endpoint_type" {
251+
type = string
252+
description = "The endpoint type to use if `existing_secrets_manager_instance_crn` is specified. Possible values: public, private."
253+
default = "private"
254+
validation {
255+
condition = contains(["public", "private"], var.existing_secrets_manager_endpoint_type)
256+
error_message = "Only \"public\" and \"private\" are allowed values for 'existing_secrets_endpoint_type'."
257+
}
258+
}
259+
260+
variable "service_credential_secrets" {
261+
type = list(object({
262+
secret_group_name = string
263+
secret_group_description = optional(string)
264+
existing_secret_group = optional(bool)
265+
service_credentials = list(object({
266+
secret_name = string
267+
service_credentials_source_service_role = string
268+
secret_labels = optional(list(string))
269+
secret_auto_rotation = optional(bool)
270+
secret_auto_rotation_unit = optional(string)
271+
secret_auto_rotation_interval = optional(number)
272+
service_credentials_ttl = optional(string)
273+
service_credential_secret_description = optional(string)
274+
275+
}))
276+
}))
277+
default = []
278+
description = "Service credential secrets configuration for Databases for Elasticsearch. [Learn more](https://github.com/terraform-ibm-modules/terraform-ibm-elasticsearch/tree/main/solutions/instance/DA-types.md#service-credential-secrets)."
279+
280+
validation {
281+
condition = alltrue([
282+
for group in var.service_credential_secrets : alltrue([
283+
for credential in group.service_credentials : contains(
284+
["Writer", "Reader", "Manager", "None"], credential.service_credentials_source_service_role
285+
)
286+
])
287+
])
288+
error_message = "service_credentials_source_service_role role must be one of 'Writer', 'Reader', 'Manager', and 'None'."
289+
290+
}
291+
}
292+
293+
variable "skip_es_sm_auth_policy" {
294+
type = bool
295+
default = false
296+
description = "Whether an IAM authorization policy is created for Secrets Manager instance to create a service credential secrets for Databases for Elasticsearch. Set to `true` to use an existing policy."
297+
}
298+
241299
variable "elser_model_type" {
242300
type = string
243301
description = "Trained ELSER model to be used for Elastic's Natural Language Processing. Possible values: `.elser_model_1`, `.elser_model_2` and `.elser_model_2_linux-x86_64`. [Learn more](https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html)"

tests/pr_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,18 +151,34 @@ func TestRunStandardSolutionSchematics(t *testing.T) {
151151

152152
// if error producing tar patterns (very unexpected) fail test immediately
153153
require.NoError(t, recurseErr, "Schematic Test had unexpected error traversing directory tree")
154-
154+
prefix := "els-sr-da"
155155
options := testschematic.TestSchematicOptionsDefault(&testschematic.TestSchematicOptions{
156156
Testing: t,
157157
TarIncludePatterns: tarIncludePatterns,
158158
TemplateFolder: standardSolutionTerraformDir,
159159
BestRegionYAMLPath: regionSelectionPath,
160-
Prefix: "els-sr-da",
160+
Prefix: prefix,
161161
ResourceGroup: resourceGroup,
162162
DeleteWorkspaceOnFail: false,
163163
WaitJobCompleteMinutes: 60,
164164
})
165165

166+
serviceCredentialSecrets := []map[string]interface{}{
167+
{
168+
"secret_group_name": fmt.Sprintf("%s-secret-group", prefix),
169+
"service_credentials": []map[string]string{
170+
{
171+
"secret_name": fmt.Sprintf("%s-cred-reader", prefix),
172+
"service_credentials_source_service_role": "Reader",
173+
},
174+
{
175+
"secret_name": fmt.Sprintf("%s-cred-writer", prefix),
176+
"service_credentials_source_service_role": "Writer",
177+
},
178+
},
179+
},
180+
}
181+
166182
options.TerraformVars = []testschematic.TestSchematicTerraformVar{
167183
{Name: "ibmcloud_api_key", Value: options.RequiredEnvironmentVars["TF_VAR_ibmcloud_api_key"], DataType: "string", Secure: true},
168184
{Name: "access_tags", Value: permanentResources["accessTags"], DataType: "list(string)"},
@@ -172,6 +188,8 @@ func TestRunStandardSolutionSchematics(t *testing.T) {
172188
{Name: "plan", Value: "platinum", DataType: "string"},
173189
{Name: "enable_elser_model", Value: true, DataType: "bool"},
174190
{Name: "service_credential_names", Value: "{\"admin_test\": \"Administrator\", \"editor_test\": \"Editor\"}", DataType: "map(string)"},
191+
{Name: "existing_secrets_manager_instance_crn", Value: permanentResources["secretsManagerCRN"], DataType: "string"},
192+
{Name: "service_credential_secrets", Value: serviceCredentialSecrets, DataType: "list(object)"},
175193
}
176194
err := options.RunSchematicTest()
177195
assert.Nil(t, err, "This should not have errored")

0 commit comments

Comments
 (0)