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 Approaches

Duration: 02:00

To successfully modify DNS records, the principal must have permission to perform both the record modification and the associated operations on Change resources.

You can configure this in one of two ways:

You can bind the IAM condition directly to the standard roles/dns.admin role. To support both the record modifications and the operations on Change resources, you use a more comprehensive CEL condition that grants access to the required non-record-set resources.

  • Permissive: Allows all other DNS administrative actions. This is equivalent to Option 2 if the complementary role contains all other standard administrative permissions. cel (resource.type == 'dns.googleapis.com/ResourceRecordSet' && <RRSET_CONDITION>) || (resource.type != 'dns.googleapis.com/ResourceRecordSet')
  • Restrictive: Only allows record set modifications and operations on Change resources. Other administrative actions, including listing record sets (dns.resourceRecordSets.list) and describing the managed zone, are blocked. cel (resource.type == 'dns.googleapis.com/ResourceRecordSet' && <RRSET_CONDITION>) || (resource.type == 'dns.googleapis.com/Change')

This approach is recommended because it doesn't require creating and managing custom roles.

Option 2: Custom Role Approach

If you prefer simpler conditions, you can 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.

Creating the Custom Roles

Duration: 05:00

[!NOTE] This step is only required if you are using Option 2: Custom Role Approach. If you are using Option 1: Standard Role Approach, you can skip this step.

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

Choose one of the following options to apply the policy.

This option uses the standard dns.admin role with a permissive condition.

cat << EOF > policy.json
{
  "bindings": [
    {
      "role": "roles/dns.admin",
      "members": ["serviceAccount:${SA_EMAIL}"],
      "condition": {
        "expression": "(resource.type == 'dns.googleapis.com/ResourceRecordSet' && resource.name.endsWith('/rrsets/api.example.com./A')) || (resource.type != 'dns.googleapis.com/ResourceRecordSet')",
        "title": "Exact Record Match (Standard Role)"
      }
    }
  ],
  "version": 3
}
EOF

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

Option 2: Custom Role Approach

This option uses the custom roles you created in the previous step.

cat << EOF > policy_custom.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 (Custom Roles)"
      }
    },
    {
      "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_custom.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

Choose one of the following options to update the policy.

cat << EOF > policy_subdomain.json
{
  "bindings": [
    {
      "role": "roles/dns.admin",
      "members": ["serviceAccount:${SA_EMAIL}"],
      "condition": {
        "expression": "(resource.type == 'dns.googleapis.com/ResourceRecordSet' && resource.name.extract('/rrsets/{name}/').endsWith('.p.example.com.')) || (resource.type != 'dns.googleapis.com/ResourceRecordSet')",
        "title": "Subdomain Delegation (Standard Role)"
      }
    }
  ],
  "version": 3
}
EOF

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

Option 2: Custom Role Approach

cat << EOF > policy_subdomain_custom.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 (Custom Roles)"
      }
    },
    {
      "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_custom.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 (only if you created them in Option 2)
gcloud iam roles delete DnsRecordSetAdmin --project=$(gcloud config get-value project) || true
gcloud iam roles delete DnsNonRecordSetAdmin --project=$(gcloud config get-value project) || true

# 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