From 0c8192756bc30fd2af17e254c65eb1634eaefa34 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Mon, 18 May 2026 14:01:12 +0100 Subject: [PATCH 01/14] Update ns1-go SDK to v2.18.0 for API key expiry support --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5fc6cf6b..f55737b6 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 github.com/stretchr/testify v1.8.1 - gopkg.in/ns1/ns1-go.v2 v2.17.2 + gopkg.in/ns1/ns1-go.v2 v2.18.0 ) require ( From 466f354e97d78d40743948fa876acc2fcbaa2637 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Mon, 18 May 2026 14:03:12 +0100 Subject: [PATCH 02/14] Add expiry_duration and secrets fields to API key resource --- ns1/resource_apikey.go | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/ns1/resource_apikey.go b/ns1/resource_apikey.go index db8d2df7..d0390baa 100644 --- a/ns1/resource_apikey.go +++ b/ns1/resource_apikey.go @@ -1,6 +1,7 @@ package ns1 import ( + "fmt" "log" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -35,6 +36,48 @@ func apikeyResource() *schema.Resource { Optional: true, Default: false, }, + "expiry_duration": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + if v != "" && v != "10d" && v != "30d" && v != "90d" { + errs = append(errs, fmt.Errorf("%q must be one of: '10d', '30d', '90d', got: %s", key, v)) + } + return + }, + Description: "Duration for automatic secret rotation. Valid values: '10d', '30d', '90d'. When set, API key secrets will automatically rotate. Changing this value will force recreation of the API key.", + }, + "secrets": { + Type: schema.TypeList, + Computed: true, + Description: "List of secrets associated with this API key when expiry_duration is set.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "The unique identifier for this secret.", + }, + "expires_at": { + Type: schema.TypeString, + Computed: true, + Description: "The expiration date/time of this secret in ISO 8601 format.", + }, + "last_access": { + Type: schema.TypeString, + Computed: true, + Description: "The last time this secret was used for authentication.", + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether this secret is currently enabled for authentication.", + }, + }, + }, + }, } s = addPermsSchema(s) @@ -70,6 +113,30 @@ func apikeyToResourceData(d *schema.ResourceData, k *account.APIKey) error { d.Set("key", k.Key) } + // Set expiry_duration if present + if k.ExpiryDuration != "" { + d.Set("expiry_duration", k.ExpiryDuration) + } + + // Set secrets if present + if len(k.Secrets) > 0 { + secrets := make([]map[string]interface{}, len(k.Secrets)) + for i, secret := range k.Secrets { + secretMap := map[string]interface{}{ + "id": secret.ID, + "expires_at": secret.ExpiresAt, + } + if secret.LastAccess != "" { + secretMap["last_access"] = secret.LastAccess + } + if secret.Enabled != nil { + secretMap["enabled"] = *secret.Enabled + } + secrets[i] = secretMap + } + d.Set("secrets", secrets) + } + return nil } @@ -102,6 +169,11 @@ func resourceDataToApikey(k *account.APIKey, d *schema.ResourceData) error { k.IPWhitelistStrict = d.Get("ip_whitelist_strict").(bool) + // Set expiry_duration if provided + if v, ok := d.GetOk("expiry_duration"); ok { + k.ExpiryDuration = v.(string) + } + return nil } From 0a67a1be75611df5daa667511c165784b3fee462 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Mon, 18 May 2026 14:03:27 +0100 Subject: [PATCH 03/14] Add tests for API key expiry_duration functionality --- ns1/resource_apikey_test.go | 103 ++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/ns1/resource_apikey_test.go b/ns1/resource_apikey_test.go index 21a23174..c353f47c 100644 --- a/ns1/resource_apikey_test.go +++ b/ns1/resource_apikey_test.go @@ -375,6 +375,109 @@ resource "ns1_apikey" "it" { `, rString, rString) } +func TestAccAPIKey_withExpiryDuration(t *testing.T) { + var apiKey account.APIKey + name := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAPIKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAPIKeyWithExpiryDuration(name, "30d"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAPIKeyExists("ns1_apikey.it", &apiKey), + testAccCheckAPIKeyName(&apiKey, name), + resource.TestCheckResourceAttr("ns1_apikey.it", "expiry_duration", "30d"), + testAccCheckAPIKeyHasSecrets(&apiKey), + ), + }, + { + ResourceName: "ns1_apikey.it", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"key", "secrets"}, // importing existing key won't have the key or secret values + }, + }, + }) +} + +func TestAccAPIKey_updateExpiryDuration(t *testing.T) { + var apiKey account.APIKey + var apiKeyRecreated account.APIKey + name := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAPIKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAPIKeyBasic(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAPIKeyExists("ns1_apikey.it", &apiKey), + testAccCheckAPIKeyName(&apiKey, name), + resource.TestCheckResourceAttr("ns1_apikey.it", "expiry_duration", ""), + ), + }, + { + Config: testAccAPIKeyWithExpiryDuration(name, "30d"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAPIKeyExists("ns1_apikey.it", &apiKeyRecreated), + testAccCheckAPIKeyName(&apiKeyRecreated, name), + resource.TestCheckResourceAttr("ns1_apikey.it", "expiry_duration", "30d"), + testAccCheckAPIKeyHasSecrets(&apiKeyRecreated), + // Verify the API key was recreated (different ID) + testAccCheckAPIKeyRecreated(&apiKey, &apiKeyRecreated), + ), + }, + }, + }) +} + +func testAccCheckAPIKeyHasSecrets(k *account.APIKey) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(k.Secrets) == 0 { + return fmt.Errorf("API key should have at least one secret when expiry_duration is set") + } + + // Verify the first secret has required fields + secret := k.Secrets[0] + if secret.ID == "" { + return fmt.Errorf("secret should have an ID") + } + if secret.ExpiresAt == "" { + return fmt.Errorf("secret should have an expiration date") + } + if secret.Enabled == nil { + return fmt.Errorf("secret should have an enabled status") + } + + return nil + } +} + +func testAccCheckAPIKeyRecreated(old, new *account.APIKey) resource.TestCheckFunc { + return func(s *terraform.State) error { + if old.ID == new.ID { + return fmt.Errorf("expected API key to be recreated with new ID, but got same ID: %s", old.ID) + } + return nil + } +} + +func testAccAPIKeyWithExpiryDuration(apiKeyName, expiryDuration string) string { + return fmt.Sprintf(`resource "ns1_apikey" "it" { + name = "%s" + expiry_duration = "%s" + + dns_view_zones = false + account_manage_users = false +} +`, apiKeyName, expiryDuration) +} + func testAccAPIKeyPermissionsNoTeam(rString string) string { return fmt.Sprintf(`resource "ns1_team" "t" { name = "terraform acc test team %s" From 4ab37084ffa93da6c7091974892242b201dca2c8 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Mon, 18 May 2026 14:03:47 +0100 Subject: [PATCH 04/14] Add example for API key with automatic secret rotation --- ns1/examples/apikey.tf | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ns1/examples/apikey.tf b/ns1/examples/apikey.tf index d7ca864e..31484394 100644 --- a/ns1/examples/apikey.tf +++ b/ns1/examples/apikey.tf @@ -7,3 +7,22 @@ resource "ns1_apikey" "apikey" { #permissions are available at the top level } + +# Example: API key with automatic secret rotation +resource "ns1_apikey" "rotating_key" { + name = "rotating-api-key" + + # Enable automatic secret rotation every 30 days + # Valid values: "10d", "30d", "90d" + expiry_duration = "30d" + + # Configure permissions + dns_view_zones = true + dns_manage_zones = true +} + +# The secrets are automatically managed and can be viewed in the state +output "rotating_key_secrets" { + value = ns1_apikey.rotating_key.secrets + sensitive = true +} From 5ff5ff6e74d31d6d09c7cdd8dbbaee68b66d8627 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Mon, 18 May 2026 14:04:01 +0100 Subject: [PATCH 05/14] Update API key documentation for expiry_duration and secrets --- website/docs/r/apikey.html.markdown | 35 ++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/website/docs/r/apikey.html.markdown b/website/docs/r/apikey.html.markdown index b9a50a8e..1c4e9135 100644 --- a/website/docs/r/apikey.html.markdown +++ b/website/docs/r/apikey.html.markdown @@ -12,6 +12,8 @@ Provides a NS1 Api Key resource. This can be used to create, modify, and delete ## Example Usage +### Basic API Key + ```hcl resource "ns1_team" "example" { name = "Example team" @@ -30,6 +32,29 @@ resource "ns1_apikey" "example" { } ``` +### API Key with Automatic Secret Rotation + +```hcl +resource "ns1_apikey" "rotating" { + name = "Rotating API Key" + + # Enable automatic secret rotation + # Secrets will expire and rotate every 30 days + # Valid values: "10d", "30d", "90d" + expiry_duration = "30d" + + # Configure permissions + dns_view_zones = true + dns_manage_zones = true +} + +# Access secret information (metadata only, not the actual secret key) +output "secret_info" { + value = ns1_apikey.rotating.secrets + sensitive = true +} +``` + ## Permissions An API key will inherit permissions from the teams it is assigned to. If a key is assigned to a team and also has individual permissions set on the key, the individual permissions @@ -50,6 +75,7 @@ The following arguments are supported: * `teams` - (Optional) The teams that the apikey belongs to. * `ip_whitelist` - (Optional, default: `[]`) Array of IP addresses/networks to which to grant the API key access. * `ip_whitelist_strict` - (Optional, default: `false`) Set to true to restrict access to only those IP addresses and networks listed in the **ip_whitelist** field. +* `expiry_duration` - (Optional) Duration for automatic secret rotation. Valid values are `"10d"`, `"30d"`, or `"90d"`. When set, the API key will use rotating secrets that expire after the specified duration. The API key can have up to 2 active secrets at a time to allow for graceful rotation. If not set, a legacy API key with a permanent secret (stored in the `key` attribute) is created. * `dns_view_zones` - (Optional, default: `false`) Whether the apikey can view the accounts zones. * `dns_manage_zones` - (Optional, default: `false`) Whether the apikey can modify the accounts zones. * `dns_zones_allow_by_default` - (Optional, default: `false`) If true, enable the `dns_zones_allow` list, otherwise enable the `dns_zones_deny` list. @@ -85,7 +111,14 @@ The following arguments are supported: In addition to all arguments above, the following attributes are exported: -* `key` - (Computed) The apikeys authentication token. +* `key` - (Computed) The API key authentication token. Only populated for legacy API keys (when `expiry_duration` is not set). For rotating API keys, use the secret keys from the `secrets` attribute instead. +* `secrets` - (Computed) List of rotating secrets for this API key. Only populated when `expiry_duration` is set. Each secret contains: + * `id` - The unique identifier for the secret. + * `expires_at` - The expiration date/time of the secret in ISO 8601 format. + * `last_access` - The last time this secret was used for authentication. + * `enabled` - Whether this secret is currently enabled for authentication. + +**Note:** The actual secret key values (starting with `nss_`) are only returned when a secret is first created and are not stored in Terraform state for security reasons. You must save these values when they are first created, as they cannot be retrieved later. ## Import From ff1838c5254ad7f59ea5271e0e9d407074b84c26 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Mon, 18 May 2026 14:04:33 +0100 Subject: [PATCH 06/14] Update CHANGELOG for v2.9.0 with API key expiry features --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4676a511..e4b1788f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.9.0 (TBD) +ENHANCEMENTS +* Add support for API key expiry and automatic secret rotation via `expiry_duration` attribute +* Add computed `secrets` attribute to view secret metadata for rotating API keys +* Update ns1-go SDK to v2.18.0 + ## 2.8.2 (February 18 2026) ENHANCEMENTS * Add insights permissions From 2ffc4fca34e1b2282dcfaee5772d76a684d2e39e Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Tue, 26 May 2026 09:57:38 +0100 Subject: [PATCH 07/14] fix docs, add error handling, add validation tests --- ns1/resource_apikey.go | 8 +++-- ns1/resource_apikey_test.go | 52 +++++++++++++++++++++++++++++ website/docs/r/apikey.html.markdown | 24 ++++++------- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/ns1/resource_apikey.go b/ns1/resource_apikey.go index d0390baa..3c25471b 100644 --- a/ns1/resource_apikey.go +++ b/ns1/resource_apikey.go @@ -115,7 +115,9 @@ func apikeyToResourceData(d *schema.ResourceData, k *account.APIKey) error { // Set expiry_duration if present if k.ExpiryDuration != "" { - d.Set("expiry_duration", k.ExpiryDuration) + if err := d.Set("expiry_duration", k.ExpiryDuration); err != nil { + return fmt.Errorf("error setting expiry_duration: %w", err) + } } // Set secrets if present @@ -134,7 +136,9 @@ func apikeyToResourceData(d *schema.ResourceData, k *account.APIKey) error { } secrets[i] = secretMap } - d.Set("secrets", secrets) + if err := d.Set("secrets", secrets); err != nil { + return fmt.Errorf("error setting secrets: %w", err) + } } return nil diff --git a/ns1/resource_apikey_test.go b/ns1/resource_apikey_test.go index c353f47c..7a6534e3 100644 --- a/ns1/resource_apikey_test.go +++ b/ns1/resource_apikey_test.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "reflect" + "regexp" "sort" "testing" @@ -477,6 +478,57 @@ func testAccAPIKeyWithExpiryDuration(apiKeyName, expiryDuration string) string { } `, apiKeyName, expiryDuration) } +func TestAccAPIKey_invalidExpiryDuration(t *testing.T) { + name := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAPIKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAPIKeyWithExpiryDuration(name, "invalid"), + ExpectError: regexp.MustCompile(`must be one of.*10d.*30d.*90d`), + }, + { + Config: testAccAPIKeyWithExpiryDuration(name, "7d"), + ExpectError: regexp.MustCompile(`must be one of.*10d.*30d.*90d`), + }, + { + Config: testAccAPIKeyWithExpiryDuration(name, "100d"), + ExpectError: regexp.MustCompile(`must be one of.*10d.*30d.*90d`), + }, + }, + }) +} + +func TestAccAPIKey_allExpiryDurations(t *testing.T) { + durations := []string{"10d", "30d", "90d"} + + for _, duration := range durations { + t.Run(duration, func(t *testing.T) { + var apiKey account.APIKey + name := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAPIKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAPIKeyWithExpiryDuration(name, duration), + Check: resource.ComposeTestCheckFunc( + testAccCheckAPIKeyExists("ns1_apikey.it", &apiKey), + testAccCheckAPIKeyName(&apiKey, name), + resource.TestCheckResourceAttr("ns1_apikey.it", "expiry_duration", duration), + testAccCheckAPIKeyHasSecrets(&apiKey), + ), + }, + }, + }) + }) + } +} func testAccAPIKeyPermissionsNoTeam(rString string) string { return fmt.Sprintf(`resource "ns1_team" "t" { diff --git a/website/docs/r/apikey.html.markdown b/website/docs/r/apikey.html.markdown index 1c4e9135..b2b441a8 100644 --- a/website/docs/r/apikey.html.markdown +++ b/website/docs/r/apikey.html.markdown @@ -12,23 +12,21 @@ Provides a NS1 Api Key resource. This can be used to create, modify, and delete ## Example Usage -### Basic API Key +### Legacy API Key (Static Secret) ```hcl -resource "ns1_team" "example" { - name = "Example team" -} - -resource "ns1_apikey" "example" { - name = "Example key" - teams = [ns1_team.example.id] - - # Optional IP whitelist - ip_whitelist = ["1.1.1.1","2.2.2.2"] +resource "ns1_apikey" "static_key" { + name = "Static API Key" # Configure permissions - dns_view_zones = false - account_manage_users = false + dns_view_zones = true + dns_manage_zones = true +} + +# The static secret is available in the key attribute +output "api_key_secret" { + value = ns1_apikey.static_key.key + sensitive = true } ``` From fdb838ef4279f3b917ea0260c55a70ae73be5771 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Tue, 26 May 2026 16:43:29 +0100 Subject: [PATCH 08/14] Update API key expiry changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4b1788f..9e764729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 2.9.0 (TBD) ENHANCEMENTS -* Add support for API key expiry and automatic secret rotation via `expiry_duration` attribute -* Add computed `secrets` attribute to view secret metadata for rotating API keys +* Add support for API key secret expiration via `expiry_duration` attribute +* Add computed `secrets` attribute to view secret metadata for API keys with expiration * Update ns1-go SDK to v2.18.0 ## 2.8.2 (February 18 2026) From 64f808a36e57a4f2e7fe3e747ce9c73579d9c94a Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Tue, 26 May 2026 16:43:37 +0100 Subject: [PATCH 09/14] Update API key expiry example --- ns1/examples/apikey.tf | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ns1/examples/apikey.tf b/ns1/examples/apikey.tf index 31484394..c64dafd3 100644 --- a/ns1/examples/apikey.tf +++ b/ns1/examples/apikey.tf @@ -8,12 +8,13 @@ resource "ns1_apikey" "apikey" { #permissions are available at the top level } -# Example: API key with automatic secret rotation -resource "ns1_apikey" "rotating_key" { - name = "rotating-api-key" +# Example: API key with secret expiration +resource "ns1_apikey" "expiring_key" { + name = "expiring-api-key" - # Enable automatic secret rotation every 30 days - # Valid values: "10d", "30d", "90d" + # Set secret expiration period to 30 days + # Secrets will expire after this period and must be manually renewed + # Accepts any duration in 'd' format (e.g., "10d", "30d", "90d") expiry_duration = "30d" # Configure permissions @@ -21,8 +22,8 @@ resource "ns1_apikey" "rotating_key" { dns_manage_zones = true } -# The secrets are automatically managed and can be viewed in the state -output "rotating_key_secrets" { - value = ns1_apikey.rotating_key.secrets +# The secrets metadata can be viewed in the state +output "expiring_key_secrets" { + value = ns1_apikey.expiring_key.secrets sensitive = true } From cb453285f6072fd2c38e27dca6560aac6479af71 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Tue, 26 May 2026 16:43:45 +0100 Subject: [PATCH 10/14] Relax expiry duration validation --- ns1/resource_apikey.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ns1/resource_apikey.go b/ns1/resource_apikey.go index 3c25471b..c9ab6bff 100644 --- a/ns1/resource_apikey.go +++ b/ns1/resource_apikey.go @@ -3,8 +3,10 @@ package ns1 import ( "fmt" "log" + "regexp" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ns1 "gopkg.in/ns1/ns1-go.v2/rest" "gopkg.in/ns1/ns1-go.v2/rest/model/account" @@ -40,14 +42,11 @@ func apikeyResource() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { - v := val.(string) - if v != "" && v != "10d" && v != "30d" && v != "90d" { - errs = append(errs, fmt.Errorf("%q must be one of: '10d', '30d', '90d', got: %s", key, v)) - } - return - }, - Description: "Duration for automatic secret rotation. Valid values: '10d', '30d', '90d'. When set, API key secrets will automatically rotate. Changing this value will force recreation of the API key.", + ValidateFunc: validation.StringMatch( + regexp.MustCompile(`^\d+d$`), + "must be a duration in format 'd' (e.g., '10d', '30d', '90d')", + ), + Description: "Duration for automatic secret expiration (e.g., '10d', '30d', '90d'). Accepts any duration in 'd' format. When set, API key secrets will automatically expire after the specified period. Changing this value will force recreation of the API key.", }, "secrets": { Type: schema.TypeList, From 09d6f18f87495cc9086919e0bf8f8604bccdfe0e Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Tue, 26 May 2026 16:43:53 +0100 Subject: [PATCH 11/14] Add custom expiry duration test --- ns1/resource_apikey_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ns1/resource_apikey_test.go b/ns1/resource_apikey_test.go index 7a6534e3..75cdd29f 100644 --- a/ns1/resource_apikey_test.go +++ b/ns1/resource_apikey_test.go @@ -530,6 +530,28 @@ func TestAccAPIKey_allExpiryDurations(t *testing.T) { } } +func TestAccAPIKey_customExpiryDuration(t *testing.T) { + var apiKey account.APIKey + name := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAPIKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAPIKeyWithExpiryDuration(name, "45d"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAPIKeyExists("ns1_apikey.it", &apiKey), + testAccCheckAPIKeyName(&apiKey, name), + resource.TestCheckResourceAttr("ns1_apikey.it", "expiry_duration", "45d"), + testAccCheckAPIKeyHasSecrets(&apiKey), + ), + }, + }, + }) +} + func testAccAPIKeyPermissionsNoTeam(rString string) string { return fmt.Sprintf(`resource "ns1_team" "t" { name = "terraform acc test team %s" From dc9cb6bfd591db270a3e75ea2cbc81ce04c5e175 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Tue, 26 May 2026 16:44:01 +0100 Subject: [PATCH 12/14] Update API key expiry docs --- website/docs/r/apikey.html.markdown | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/website/docs/r/apikey.html.markdown b/website/docs/r/apikey.html.markdown index b2b441a8..54471d08 100644 --- a/website/docs/r/apikey.html.markdown +++ b/website/docs/r/apikey.html.markdown @@ -30,15 +30,15 @@ output "api_key_secret" { } ``` -### API Key with Automatic Secret Rotation +### API Key with Secret Expiration ```hcl -resource "ns1_apikey" "rotating" { - name = "Rotating API Key" +resource "ns1_apikey" "expiring" { + name = "Expiring API Key" - # Enable automatic secret rotation - # Secrets will expire and rotate every 30 days - # Valid values: "10d", "30d", "90d" + # Set secret expiration period to 30 days + # Secrets will expire after this period and must be manually rotated + # Accepts any duration in 'd' format (e.g., "10d", "30d", "90d") expiry_duration = "30d" # Configure permissions @@ -46,13 +46,17 @@ resource "ns1_apikey" "rotating" { dns_manage_zones = true } -# Access secret information (metadata only, not the actual secret key) +# Access secret metadata (not the actual secret key values) output "secret_info" { - value = ns1_apikey.rotating.secrets + value = ns1_apikey.expiring.secrets sensitive = true } ``` +## Important Notes + +~> **Changing expiry_duration forces recreation.** When you modify the `expiry_duration` field of an existing API key, Terraform will destroy the old key and create a new one. This means the API key ID and all secrets will change. Any external references to the old key will break. Plan your migrations carefully and update dependent systems before changing this value. + ## Permissions An API key will inherit permissions from the teams it is assigned to. If a key is assigned to a team and also has individual permissions set on the key, the individual permissions @@ -73,7 +77,7 @@ The following arguments are supported: * `teams` - (Optional) The teams that the apikey belongs to. * `ip_whitelist` - (Optional, default: `[]`) Array of IP addresses/networks to which to grant the API key access. * `ip_whitelist_strict` - (Optional, default: `false`) Set to true to restrict access to only those IP addresses and networks listed in the **ip_whitelist** field. -* `expiry_duration` - (Optional) Duration for automatic secret rotation. Valid values are `"10d"`, `"30d"`, or `"90d"`. When set, the API key will use rotating secrets that expire after the specified duration. The API key can have up to 2 active secrets at a time to allow for graceful rotation. If not set, a legacy API key with a permanent secret (stored in the `key` attribute) is created. +* `expiry_duration` - (Optional) Duration for secret expiration in `d` format (e.g., `"10d"`, `"30d"`, `"90d"`). When set, API key secrets will expire after the specified period and must be manually rotated using the NS1 API or Portal. The API key can have up to 2 active secrets at a time to allow for graceful rotation without service interruption. If not set, a legacy API key with a permanent secret (stored in the `key` attribute) is created. Changing this value will force recreation of the API key. * `dns_view_zones` - (Optional, default: `false`) Whether the apikey can view the accounts zones. * `dns_manage_zones` - (Optional, default: `false`) Whether the apikey can modify the accounts zones. * `dns_zones_allow_by_default` - (Optional, default: `false`) If true, enable the `dns_zones_allow` list, otherwise enable the `dns_zones_deny` list. @@ -109,14 +113,14 @@ The following arguments are supported: In addition to all arguments above, the following attributes are exported: -* `key` - (Computed) The API key authentication token. Only populated for legacy API keys (when `expiry_duration` is not set). For rotating API keys, use the secret keys from the `secrets` attribute instead. -* `secrets` - (Computed) List of rotating secrets for this API key. Only populated when `expiry_duration` is set. Each secret contains: +* `key` - (Computed) The API key authentication token. Only populated for legacy API keys (when `expiry_duration` is not set). For API keys with expiration, use the secret keys from the `secrets` attribute instead. +* `secrets` - (Computed) List of secrets for this API key. Only populated when `expiry_duration` is set. Each secret contains: * `id` - The unique identifier for the secret. * `expires_at` - The expiration date/time of the secret in ISO 8601 format. * `last_access` - The last time this secret was used for authentication. * `enabled` - Whether this secret is currently enabled for authentication. -**Note:** The actual secret key values (starting with `nss_`) are only returned when a secret is first created and are not stored in Terraform state for security reasons. You must save these values when they are first created, as they cannot be retrieved later. +**Note:** The actual secret key values (starting with `nss_`) are only returned when a secret is first created and are not stored in Terraform state for security reasons. You must save these values when they are first created, as they cannot be retrieved later. To rotate secrets (generate new ones), use the NS1 API or Portal - Terraform does not manage secret rotation. ## Import From ebb01234f25948e4bc733280d505cd0877c18c68 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Thu, 11 Jun 2026 12:04:05 +0100 Subject: [PATCH 13/14] update the release date --- CHANGELOG.md | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e764729..ec39d197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.9.0 (TBD) +## 2.9.0 (June 11, 2026) ENHANCEMENTS * Add support for API key secret expiration via `expiry_duration` attribute * Add computed `secrets` attribute to view secret metadata for API keys with expiration diff --git a/go.sum b/go.sum index 72f49a18..37a340a9 100644 --- a/go.sum +++ b/go.sum @@ -275,8 +275,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ns1/ns1-go.v2 v2.17.2 h1:x8YKHqCJWkC/hddfUhw7FRqTG0x3fr/0ZnWYN+i4THs= -gopkg.in/ns1/ns1-go.v2 v2.17.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= +gopkg.in/ns1/ns1-go.v2 v2.18.0 h1:dz5QvZGd9nLSp7hktmJwXgCI2L7pcRN/mtZxyNim370= +gopkg.in/ns1/ns1-go.v2 v2.18.0/go.mod h1:hDx6jxNZBmEOkpV8buB2eR1rxlgYUgRDT3nRHi8RWWc= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 5fb2497b8eab02c88fa97513394f1f55175cd164 Mon Sep 17 00:00:00 2001 From: soniafrancisNS1 Date: Thu, 11 Jun 2026 12:12:34 +0100 Subject: [PATCH 14/14] Fix validation test to accept any days --- ns1/resource_apikey_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ns1/resource_apikey_test.go b/ns1/resource_apikey_test.go index 75cdd29f..b128614b 100644 --- a/ns1/resource_apikey_test.go +++ b/ns1/resource_apikey_test.go @@ -488,15 +488,15 @@ func TestAccAPIKey_invalidExpiryDuration(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccAPIKeyWithExpiryDuration(name, "invalid"), - ExpectError: regexp.MustCompile(`must be one of.*10d.*30d.*90d`), + ExpectError: regexp.MustCompile(`must be a duration in format 'd'`), }, { - Config: testAccAPIKeyWithExpiryDuration(name, "7d"), - ExpectError: regexp.MustCompile(`must be one of.*10d.*30d.*90d`), + Config: testAccAPIKeyWithExpiryDuration(name, "30"), + ExpectError: regexp.MustCompile(`must be a duration in format 'd'`), }, { - Config: testAccAPIKeyWithExpiryDuration(name, "100d"), - ExpectError: regexp.MustCompile(`must be one of.*10d.*30d.*90d`), + Config: testAccAPIKeyWithExpiryDuration(name, "d30"), + ExpectError: regexp.MustCompile(`must be a duration in format 'd'`), }, }, })