Cloud DNS Granular IAM Permissions for Record Sets


This codelab demonstrates how to implement fine-grained access control for individual DNS record sets within Cloud DNS using IAM Conditions and custom roles.

Introduction

Cloud DNS traditionally supports setting IAM permissions at the project and managed zone levels. This provides broad access to all records within a zone. However, for large enterprises, this doesn't meet the "principle of least privilege."

This codelab will guide you through configuring per-record-set IAM permissions in Cloud DNS. This feature allows you to delegate management of specific subdomains or record types to different teams within a single shared managed zone.

What you'll learn

  • How to use IAM Conditions to restrict DNS record management.
  • Why and how to create custom roles.
  • How to delegate subdomains and specific record types (for example: A, MX).
  • How to handle DNS transactions with conditional permissions using the --skip-soa-update flag.

Prerequisites

  • A Google Account
  • A Google Cloud project with billing enabled
  • The latest version of the Google Cloud CLI installed and configured
  • A basic understanding of DNS and IAM concepts

Getting set up

Duration: 03:00

Enable the Cloud DNS API

Log in to the gcloud CLI and enable the API.

gcloud auth login
gcloud services enable dns.googleapis.com

Create a test project

If you don't have a project ready, create one now and set your gcloud configuration to the project so all commands will target that project.

gcloud projects create my-dns-per-rrset-lab
gcloud config set project my-dns-per-rrset-lab

Understanding the Custom Role Approach

Duration: 02:00

You cannot bind IAM conditions directly to the standard roles/dns.admin role. Instead, you must separate record set management from other administrative tasks using two custom roles:

  1. DnsRecordSetAdmin: Contains permissions to create, delete, get, and update resource record sets. This role will be granted conditionally.
  2. DnsNonRecordSetAdmin: Contains all other DNS administrative permissions (like managing zones, listing records, and viewing project details). This role will be granted unconditionally.

By splitting these roles, you ensure that the prerequisite checks performed by Cloud DNS (like dns.changes.create) are satisfied unconditionally, while the actual record modifications are strictly controlled by your conditions.

Creating the Custom Roles

Duration: 05:00

Run the following commands to create the required custom roles in your project.

Define the permission sets

# Record set management permissions
rs_perms="dns.resourceRecordSets.create,dns.resourceRecordSets.delete,dns.resourceRecordSets.get,dns.resourceRecordSets.update"

# Complementary administrative permissions
comp_perms="compute.networks.get,compute.networks.list,dns.changes.create,dns.changes.get,dns.changes.list,dns.dnsKeys.get,dns.dnsKeys.list,dns.gkeClusters.bindDNSResponsePolicy,dns.gkeClusters.bindPrivateDNSZone,dns.managedZoneOperations.get,dns.managedZoneOperations.list,dns.managedZones.create,dns.managedZones.delete,dns.managedZones.get,dns.managedZones.getIamPolicy,dns.managedZones.list,dns.managedZones.update,dns.networks.bindDNSResponsePolicy,dns.networks.bindPrivateDNSPolicy,dns.networks.bindPrivateDNSZone,dns.networks.targetWithPeeringZone,dns.networks.useHealthSignals,dns.policies.create,dns.policies.createTagBinding,dns.policies.delete,dns.policies.deleteTagBinding,dns.policies.get,dns.policies.list,dns.policies.listEffectiveTags,dns.policies.listTagBindings,dns.policies.update,dns.projects.get,dns.resourceRecordSets.list,dns.responsePolicies.create,dns.responsePolicies.delete,dns.responsePolicies.get,dns.responsePolicies.list,dns.responsePolicies.update,dns.responsePolicyRules.create,dns.responsePolicyRules.delete,dns.responsePolicyRules.get,dns.responsePolicyRules.list,dns.responsePolicyRules.update,resourcemanager.projects.get"

Create the roles in Gcloud

gcloud iam roles create DnsRecordSetAdmin --project=$(gcloud config get-value project) \
    --title="DNS Record Set Admin (Conditional)" --permissions="${rs_perms}"

gcloud iam roles create DnsNonRecordSetAdmin --project=$(gcloud config get-value project) \
    --title="DNS Complimentary Admin" --permissions="${comp_perms}"

Scenario 1: Exact Record Match

Duration: 05:00

In this scenario, you want to grant a team permission to manage only the A record for api.example.com..

Create a managed zone

gcloud dns managed-zones create example-zone \
    --description="Lab zone for per-RRSet permissions" \
    --dns-name=example.com. --visibility=private \
    --networks=default

Create a test service account

You will use this service account to verify the restricted permissions.

gcloud iam service-accounts create dns-restricted-sa \
    --display-name="Restricted DNS SA"

SA_EMAIL="dns-restricted-sa@$(gcloud config get-value project).iam.gserviceaccount.com"

Apply the conditional IAM policy

Grant DnsNonRecordSetAdmin unconditionally and DnsRecordSetAdmin with a condition.

cat << EOF > policy.json
{
  "bindings": [
    {
      "role": "projects/$(gcloud config get-value project)/roles/DnsRecordSetAdmin",
      "members": ["serviceAccount:${SA_EMAIL}"],
      "condition": {
        "expression": "resource.type == 'dns.googleapis.com/ResourceRecordSet' && resource.name.endsWith('/rrsets/api.example.com./A')",
        "title": "Exact Record Match"
      }
    },
    {
      "role": "projects/$(gcloud config get-value project)/roles/DnsNonRecordSetAdmin",
      "members": ["serviceAccount:${SA_EMAIL}"]
    }
  ],
  "version": 3
}
EOF

gcloud dns managed-zones set-iam-policy example-zone --policy-file=policy.json

Verify the restriction

Try to create the allowed record, then try an unauthorized one.

# ALLOWED: Create the specific A record
gcloud dns record-sets create api.example.com. --zone=example-zone --type=A --rrdatas="1.2.3.4" --ttl=300 --impersonate-service-account=${SA_EMAIL}

# DENIED: Create an unauthorized name
gcloud dns record-sets create www.example.com. --zone=example-zone --type=A --rrdatas="5.6.7.8" --ttl=300 --impersonate-service-account=${SA_EMAIL}

Scenario 2: Subdomain Delegation

Duration: 05:00

Now, let's grant permission to manage any record within the p.example.com. subdomain.

Update the IAM policy

Modify the condition to use resource.name.extract() to match the subdomain suffix.

cat << EOF > policy_subdomain.json
{
  "bindings": [
    {
      "role": "projects/$(gcloud config get-value project)/roles/DnsRecordSetAdmin",
      "members": ["serviceAccount:${SA_EMAIL}"],
      "condition": {
        "expression": "resource.type == 'dns.googleapis.com/ResourceRecordSet' && resource.name.extract('/rrsets/{name}/').endsWith('.p.example.com.')",
        "title": "Subdomain Delegation"
      }
    },
    {
      "role": "projects/$(gcloud config get-value project)/roles/DnsNonRecordSetAdmin",
      "members": ["serviceAccount:${SA_EMAIL}"]
    }
  ],
  "version": 3
}
EOF

gcloud dns managed-zones set-iam-policy example-zone --policy-file=policy_subdomain.json

Verify the delegation

# ALLOWED: Create any record in the subdomain
gcloud dns record-sets create test.p.example.com. --zone=example-zone --type=A --rrdatas="192.168.1.1" --ttl=300 --impersonate-service-account=${SA_EMAIL}

# DENIED: Create a record outside the subdomain
gcloud dns record-sets create news.example.com. --zone=example-zone --type=A --rrdatas="192.168.1.2" --ttl=300 --impersonate-service-account=${SA_EMAIL}

Scenario 3: Batch Changes and Transactions

Duration: 03:00

When using conditional permissions, there is an important detail for transactions. By default, DNS transactions attempt to update the SOA record. If your IAM condition only allows users to manage specific records (like api.example.com.), the transaction will fail because the user is not authorized to modify the SOA record.

The --skip-soa-update Flag

To modify allowed records within a transaction, you should either allow SOA updates by modifying your condition accordingly (resource.name.endsWith('/SOA')) or use the --skip-soa-update flag.

gcloud dns record-sets transaction start --zone=example-zone --skip-soa-update
gcloud dns record-sets transaction add --zone=example-zone --name="api.example.com." --type=A --ttl=300 "10.0.0.1"
gcloud dns record-sets transaction execute --zone=example-zone --impersonate-service-account=${SA_EMAIL}

Note: If a transaction contains even one unauthorized record modification, the entire transaction will be rejected.

Clean up

Duration: 01:00

Delete the resources created in this lab to avoid charges.

# Delete record sets
gcloud dns record-sets delete api.example.com. --zone=example-zone --type=A
gcloud dns record-sets delete test.p.example.com. --zone=example-zone --type=A

# Delete managed zone
gcloud dns managed-zones delete example-zone

# Delete custom roles
gcloud iam roles delete DnsRecordSetAdmin --project=$(gcloud config get-value project)
gcloud iam roles delete DnsNonRecordSetAdmin --project=$(gcloud config get-value project)

# Delete service account
gcloud iam service-accounts delete ${SA_EMAIL}

Congratulations

Congratulations! You have successfully learned how to implement granular, per-record-set IAM permissions in Cloud DNS.

Summary of what we covered

  • Created custom roles to separate record-set permissions from zone-level administrative tasks.
  • Implemented an Exact Match condition for specific record names and types.
  • Implemented Subdomain Delegation using string extraction in IAM conditions.
  • Used the --skip-soa-update flag to allow conditional users to perform batch changes.

Further reading