Skip to content

Commit

Permalink
Terraform Data Source to get DNSKEY records of DNSSEC-signed managed …
Browse files Browse the repository at this point in the history
…zones (#3117) (#1768)

* DNSSEC Keys

* update schema

* Define as a data source

* add into data sources map

* remove unused import

* add tests

* No fill dns keys of zone is not dnssec enabled

* add docs

* add ds record to ksk

* fix string templating

* improve doc description

* improve ds record generation

* Update third_party/terraform/website/docs/d/datasource_dns_key.html.markdown

Co-Authored-By: Sam Levenick <[email protected]>

* rename data source in plural

* rename file, add comment on maps

* rename doc file

Co-authored-by: Sam Levenick <[email protected]>
Signed-off-by: Modular Magician <[email protected]>

Co-authored-by: Sam Levenick <[email protected]>
  • Loading branch information
modular-magician and slevenick authored Feb 18, 2020
1 parent c77a090 commit d91b1cf
Show file tree
Hide file tree
Showing 6 changed files with 388 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/3117.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-datasource
google_dns_keys
```
92 changes: 92 additions & 0 deletions google-beta/data_source_dns_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package google

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
)

func TestAccDataSourceDNSKeys_basic(t *testing.T) {
t.Parallel()

dnsZoneName := fmt.Sprintf("data-dnskey-test-%s", acctest.RandString(10))

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDNSManagedZoneDestroy,
Steps: []resource.TestStep{
{
Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "on"),
Check: resource.ComposeTestCheckFunc(
testAccDataSourceDNSKeysDSRecordCheck("data.google_dns_keys.foo_dns_key"),
resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.#", "1"),
resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "zone_signing_keys.#", "1"),
resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key_id", "key_signing_keys.#", "1"),
resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key_id", "zone_signing_keys.#", "1"),
),
},
},
})
}

func TestAccDataSourceDNSKeys_noDnsSec(t *testing.T) {
t.Parallel()

dnsZoneName := fmt.Sprintf("data-dnskey-test-%s", acctest.RandString(10))

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDNSManagedZoneDestroy,
Steps: []resource.TestStep{
{
Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "off"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.#", "0"),
resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "zone_signing_keys.#", "0"),
),
},
},
})
}

func testAccDataSourceDNSKeysDSRecordCheck(datasourceName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
ds, ok := s.RootModule().Resources[datasourceName]
if !ok {
return fmt.Errorf("root module has no resource called %s", datasourceName)
}

if ds.Primary.Attributes["key_signing_keys.0.ds_record"] == "" {
return fmt.Errorf("DS record not found in data source")
}

return nil
}
}

func testAccDataSourceDNSKeysConfig(dnsZoneName, dnssecStatus string) string {
return fmt.Sprintf(`
resource "google_dns_managed_zone" "foo" {
name = "%s"
dns_name = "dnssec.tf-test.club."
dnssec_config {
state = "%s"
non_existence = "nsec3"
}
}
data "google_dns_keys" "foo_dns_key" {
managed_zone = google_dns_managed_zone.foo.name
}
data "google_dns_keys" "foo_dns_key_id" {
managed_zone = google_dns_managed_zone.foo.id
}
`, dnsZoneName, dnssecStatus)
}
219 changes: 219 additions & 0 deletions google-beta/data_source_dns_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package google

import (
"fmt"
"log"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"google.golang.org/api/dns/v1"
)

// DNSSEC Algorithm Numbers: https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
// The following are algorithms that are supported by Cloud DNS
var dnssecAlgoNums = map[string]int{
"rsasha1": 5,
"rsasha256": 8,
"rsasha512": 10,
"ecdsap256sha256": 13,
"ecdsap384sha384": 14,
}

// DS RR Digest Types: https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml
// The following are digests that are supported by Cloud DNS
var dnssecDigestType = map[string]int{
"sha1": 1,
"sha256": 2,
"sha384": 4,
}

func dataSourceDNSKeys() *schema.Resource {
return &schema.Resource{
Read: dataSourceDNSKeysRead,

Schema: map[string]*schema.Schema{
"managed_zone": {
Type: schema.TypeString,
Required: true,
DiffSuppressFunc: compareSelfLinkOrResourceName,
},
"project": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"key_signing_keys": {
Type: schema.TypeList,
Computed: true,
Elem: kskResource(),
},
"zone_signing_keys": {
Type: schema.TypeList,
Computed: true,
Elem: dnsKeyResource(),
},
},
}
}

func dnsKeyResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"algorithm": {
Type: schema.TypeString,
Computed: true,
},
"creation_time": {
Type: schema.TypeString,
Computed: true,
},
"description": {
Type: schema.TypeString,
Computed: true,
},
"digests": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"digest": {
Type: schema.TypeString,
Optional: true,
},
"type": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
"id": {
Type: schema.TypeString,
Computed: true,
},
"is_active": {
Type: schema.TypeBool,
Computed: true,
},
"key_length": {
Type: schema.TypeInt,
Computed: true,
},
"key_tag": {
Type: schema.TypeInt,
Computed: true,
},
"public_key": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func kskResource() *schema.Resource {
resource := dnsKeyResource()

resource.Schema["ds_record"] = &schema.Schema{
Type: schema.TypeString,
Computed: true,
}

return resource
}

func generateDSRecord(signingKey *dns.DnsKey) (string, error) {
algoNum, found := dnssecAlgoNums[signingKey.Algorithm]
if !found {
return "", fmt.Errorf("DNSSEC Algorithm number for %s not found", signingKey.Algorithm)
}

digestType, found := dnssecDigestType[signingKey.Digests[0].Type]
if !found {
return "", fmt.Errorf("DNSSEC Digest type for %s not found", signingKey.Digests[0].Type)
}

return fmt.Sprintf("%d %d %d %s",
signingKey.KeyTag,
algoNum,
digestType,
signingKey.Digests[0].Digest), nil
}

func flattenSigningKeys(signingKeys []*dns.DnsKey, keyType string) []map[string]interface{} {
var keys []map[string]interface{}

for _, signingKey := range signingKeys {
if signingKey != nil && signingKey.Type == keyType {
data := map[string]interface{}{
"algorithm": signingKey.Algorithm,
"creation_time": signingKey.CreationTime,
"description": signingKey.Description,
"digests": flattenDigests(signingKey.Digests),
"id": signingKey.Id,
"is_active": signingKey.IsActive,
"key_length": signingKey.KeyLength,
"key_tag": signingKey.KeyTag,
"public_key": signingKey.PublicKey,
}

if signingKey.Type == "keySigning" && len(signingKey.Digests) > 0 {
dsRecord, err := generateDSRecord(signingKey)
if err == nil {
data["ds_record"] = dsRecord
}
}

keys = append(keys, data)
}
}

return keys
}

func flattenDigests(dnsKeyDigests []*dns.DnsKeyDigest) []map[string]interface{} {
var digests []map[string]interface{}

for _, dnsKeyDigest := range dnsKeyDigests {
if dnsKeyDigest != nil {
data := map[string]interface{}{
"digest": dnsKeyDigest.Digest,
"type": dnsKeyDigest.Type,
}

digests = append(digests, data)
}
}

return digests
}

func dataSourceDNSKeysRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

fv, err := parseProjectFieldValue("managedZones", d.Get("managed_zone").(string), "project", d, config, false)
if err != nil {
return err
}
project := fv.Project
managedZone := fv.Name

d.Set("project", project)
d.SetId(fmt.Sprintf("projects/%s/managedZones/%s", project, managedZone))

log.Printf("[DEBUG] Fetching DNS keys from managed zone %s", managedZone)

response, err := config.clientDns.DnsKeys.List(project, managedZone).Do()
if err != nil && !isGoogleApiErrorWithCode(err, 404) {
return fmt.Errorf("error retrieving DNS keys: %s", err)
} else if isGoogleApiErrorWithCode(err, 404) {
return nil
}

log.Printf("[DEBUG] Fetched DNS keys from managed zone %s", managedZone)

d.Set("key_signing_keys", flattenSigningKeys(response.DnsKeys, "keySigning"))
d.Set("zone_signing_keys", flattenSigningKeys(response.DnsKeys, "zoneSigning"))

return nil
}
1 change: 1 addition & 0 deletions google-beta/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ func Provider() terraform.ResourceProvider {
"google_container_engine_versions": dataSourceGoogleContainerEngineVersions(),
"google_container_registry_image": dataSourceGoogleContainerImage(),
"google_container_registry_repository": dataSourceGoogleContainerRepo(),
"google_dns_keys": dataSourceDNSKeys(),
"google_dns_managed_zone": dataSourceDnsManagedZone(),
"google_iam_policy": dataSourceGoogleIamPolicy(),
"google_iam_role": dataSourceGoogleIamRole(),
Expand Down
70 changes: 70 additions & 0 deletions website/docs/d/datasource_dns_keys.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
subcategory: "Cloud DNS"
layout: "google"
page_title: "Google: google_dns_keys"
sidebar_current: "docs-google-datasource-dns-keys"
description: |-
Get DNSKEY and DS records of DNSSEC-signed managed zones.
---

# google\_dns\_keys

Get the DNSKEY and DS records of DNSSEC-signed managed zones. For more information see the
[official documentation](https://cloud.google.com/dns/docs/dnskeys/)
and [API](https://cloud.google.com/dns/docs/reference/v1/dnsKeys).


## Example Usage

```hcl
resource "google_dns_managed_zone" "foo" {
name = "foobar"
dns_name = "foo.bar."
dnssec_config {
state = "on"
non_existence = "nsec3"
}
}
data "google_dns_keys" "foo_dns_keys" {
managed_zone = google_dns_managed_zone.foo.id
}
output "foo_dns_ds_record" {
description = "DS record of the foo subdomain."
value = data.google_dns_keys.foo_dns_keys.key_signing_keys[0].ds_record
}
```

## Argument Reference

The following arguments are supported:

* `managed_zone` - (Required) The name or id of the Cloud DNS managed zone.

* `project` - (Optional) The ID of the project in which the resource belongs. If `project` is not provided, the provider project is used.

## Attributes Reference

The following attributes are exported:

* `key_signing_keys` - A list of Key-signing key (KSK) records. Structure is documented below. Additionally, the DS record is provided:
* `ds_record` - The DS record based on the KSK record. This is used when [delegating](https://cloud.google.com/dns/docs/dnssec-advanced#subdelegation) DNSSEC-signed subdomains.

* `zone_signing_keys` - A list of Zone-signing key (ZSK) records. Structure is documented below.

---

The `key_signing_keys` and `zone_signing_keys` block supports:
* `algorithm` - String mnemonic specifying the DNSSEC algorithm of this key. Immutable after creation time. Possible values are `ecdsap256sha256`, `ecdsap384sha384`, `rsasha1`, `rsasha256`, and `rsasha512`.
* `creation_time` - The time that this resource was created in the control plane. This is in RFC3339 text format.
* `description` - A mutable string of at most 1024 characters associated with this resource for the user's convenience.
* `digests` - A list of cryptographic hashes of the DNSKEY resource record associated with this DnsKey. These digests are needed to construct a DS record that points at this DNS key. Each contains:
- `digest` - The base-16 encoded bytes of this digest. Suitable for use in a DS resource record.
- `type` - Specifies the algorithm used to calculate this digest. Possible values are `sha1`, `sha256` and `sha384`
* `id` - Unique identifier for the resource; defined by the server.
* `is_active` - Active keys will be used to sign subsequent changes to the ManagedZone. Inactive keys will still be present as DNSKEY Resource Records for the use of resolvers validating existing signatures.
* `key_length` - Length of the key in bits. Specified at creation time then immutable.
* `key_tag` - The key tag is a non-cryptographic hash of the a DNSKEY resource record associated with this DnsKey. The key tag can be used to identify a DNSKEY more quickly (but it is not a unique identifier). In particular, the key tag is used in a parent zone's DS record to point at the DNSKEY in this child ManagedZone. The key tag is a number in the range [0, 65535] and the algorithm to calculate it is specified in RFC4034 Appendix B.
* `public_key` - Base64 encoded public half of this key.
Loading

0 comments on commit d91b1cf

Please sign in to comment.