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-updateflag.
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:
Option 1: Standard Role Approach (Recommended)
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:
- DnsRecordSetAdmin: Contains permissions to create, delete, get, and update resource record sets. This role will be granted conditionally.
- 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.
Option 1: Standard Role Approach (Recommended)
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.
Option 1: Standard Role Approach (Recommended)
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-updateflag to allow conditional users to perform batch changes.