Skip to content

Active Directory Domain Service

Microsoft Entra Domain Services (formerly Azure AD Domain Services, AADDS) provides a managed domain with classic Active Directory features — domain join, group policy, LDAP, and Kerberos/NTLM authentication — without you having to deploy, patch, or manage domain controllers. The active_directory_domain_service module creates and configures the managed domain in a dedicated subnet.

Note: A managed domain is a tenant-level singleton — you can create only one managed domain per Microsoft Entra tenant. The input variable is a map of objects to match the repo convention, but it should hold exactly one entry. Multi-region presence is achieved with replica sets (the same domain extended to other regions), not by deploying a second managed domain; replica sets are out of scope for this module.

Prerequisites

AADDS depends on a subscription-level registration and on tenant-level Microsoft Entra objects that this azurerm-only repository does not manage. Complete these before applying:

  1. Register the Microsoft.AAD resource provider — managed-domain creation fails until this is registered on the subscription:
    az provider register --namespace Microsoft.AAD --subscription <SUBSCRIPTION_ID>
    az provider show -n Microsoft.AAD --query registrationState -o tsv   # wait for "Registered"
    
  2. Domain Services service principal — a service principal for the well-known Domain Services application must exist in the tenant. The portal wizard creates it automatically, but the azurerm resource does not, so create it once. The application ID is a fixed Microsoft constant — 2565bd9d-da50-47d4-8b85-4c97f669dc36 for Azure Global (6ba9a5d4-8456-4118-b521-9c5ca10cdf84 for sovereign clouds):
    az ad sp create --id 2565bd9d-da50-47d4-8b85-4c97f669dc36
    
  3. AAD DC Administrators group — a security group named AAD DC Administrators whose members are granted administrative rights on the managed domain. Add at least one member.
  4. A user with a synchronized password — Entra ID does not store Kerberos/NTLM-compatible hashes until AADDS is enabled, and hashes only sync forward. Any account that must authenticate against the managed domain has to reset its password after the domain is provisioned. (Deleting and recreating the domain deletes stored hashes, so this repeats each rebuild.)
  5. Networking — a dedicated, non-delegated subnet with an NSG, outbound connectivity, and (post-deploy) VNet DNS pointed at the domain controllers. See Networking.

See the Microsoft prerequisites documentation for details.

Basic Configuration

The module ships secure, idempotent defaults, so a working managed domain needs only three fields:

active_directory_domain_service = {
    aadds = {
        resource_group = "hsw"
        domain_name    = "aadds.sapphirehealth.org"

        initial_replica_set = {
            subnet = "hsw.aadds"
        }
    }
}

This creates a managed domain with these defaults applied automatically:

  • SKU: Standard (single-region; no replica sets)
  • Domain configuration type: FullySynced (set explicitly to avoid a forced replacement on later plans — see the note under Configuration Parameters)
  • Security: TLS 1.2 enforced (tls_v1_enabled = false), legacy NTLM v1 disabled, and Kerberos + NTLM password-hash sync enabled
  • Notifications: notify the AAD DC Administrators group on alerts (global admins not notified)
  • Filtered sync: Disabled
  • Location: Uses the location defined in the main configuration

Networking

The managed domain needs a correctly-shaped subnet, an NSG, outbound connectivity, and VNet DNS pointed at its domain controllers. The subnet, NSG, rules, NAT gateway, and DNS are all created by existing modules — only the wiring below is AADDS-specific.

Subnet

Use a dedicated subnet (Microsoft requires the managed domain to be alone in its subnet). It must not be delegated and not carry a custom route table that overrides the 0.0.0.0/0 route. A /24 is recommended (the domain needs 3–5 IPs; the DCs take the first two, e.g. .4 and .5).

# networks.tfvars — inside the VNet's subnets map
aadds = {
    network_security_group = "aadds"        # see NSG below
    address_prefixes       = ["10.40.46.0/24"]
    nat_gateway            = "hsw"           # outbound — see below
}

NSG and required rule

Create a dedicated NSG for the subnet with a single required inbound rule: WinRM TCP 5986 from the AzureActiveDirectoryDomainServices service tag (used by the AADDS management plane). The Azure default rules AllowVnetInBound and AllowAzureLoadBalancerInBound cover the rest.

# rules.tfvars
AADDS_PSRemoting_Inbound = {
    destination_port_ranges = ["5986"]
    source_address_prefix   = "AzureActiveDirectoryDomainServices"
}

# nsgs.tfvars
aadds = {
    resource_group = "hsw"
    rules = {
        AADDS_PSRemoting_Inbound = "110"
    }
}

Outbound connectivity

AADDS requires outbound 443 to several service tags (AzureActiveDirectoryDomainServices, AzureMonitor, Storage, AzureActiveDirectory, GuestAndHybridManagement). New subnets are created with default_outbound_access_enabled = false (azurerm 4.x is private-by-default), so attach a NAT gateway to the subnet (nat_gateway = "hsw" above) to guarantee outbound. A NAT gateway is supported with AADDS — it is not an NSG or UDR, so it does not trigger the unsupported-state warnings, and it affects only outbound (inbound management via the AADDS-managed load balancer is unaffected).

DNS

For VMs and apps to use the managed domain (domain-join, Kerberos/LDAP), the VNet's DNS must point at the domain controllers.

The virtual_network_dns_servers module sets this automatically from the domain service output — no hardcoded IPs. Opt a VNet in by naming the AADDS entry's key:

# networks.tfvars — on the VNet (network) entry, not the subnet
hsw = {
    resource_group = "hsw"
    address_space  = ["10.40.44.0/22"]
    active_directory_domain_service = "aadds"   # -> VNet DNS = that domain's DC IPs
    subnets = { ... }
}

How it works and why it's safe:

  • The DC IPs are read from module.active_directory_domain_service output, so they always track the live values.
  • No dependency cycle: DNS is a downstream resource (virtual_network → subnet → domain service → dns_servers). Setting dns_servers inline on the VNet instead would cycle.
  • Correct ordering: because the DNS resource depends on the domain, Terraform applies it after the domain provisions, so the VNet has default DNS during creation (pointing VNet DNS at not-yet-existent DCs can otherwise block creation).

For a VNet that needs a manual DNS list instead (no AADDS), set dns_servers = ["x.x.x.x", ...] on the network entry. If both are set, the AADDS DC IPs win.

Configuration Parameters

Parameter Type Required Default Description
resource_group string Yes - Key of the resource group where the managed domain is deployed
domain_name string Yes* - The DNS domain name for the managed domain (e.g. aadds.contoso.com). Forces replacement if changed. *Not required when existing = true
initial_replica_set object Yes* - The initial replica set placement (see Initial Replica Set). *Not required when existing = true
name string No Auto-generated Display name of the managed domain resource. If omitted, uses the naming convention
location string No Global location Azure region where the managed domain is created
existing bool No false Reference an existing managed domain via a data source instead of creating one (see Referencing an Existing Domain)
sku string No Standard SKU: Standard, Enterprise, or Premium. Standard does not support replica sets
domain_configuration_type string No FullySynced FullySynced or ResourceTrusting. Forces replacement if changed (see note below)
filtered_sync_enabled bool No false Enables group-based (scoped) synchronization
secure_ldap object No omitted Secure LDAP (LDAPS) configuration (see Secure LDAP)
notifications object No {} (defaults applied) Alert notification settings (see Notifications)
security object No {} (defaults applied) Password sync and legacy protocol settings (see Security)
timeouts object No omitted Custom Terraform operation timeouts (create, read, update, delete). The azurerm default create timeout is 3 hours
tags map(string) No {} Tags merged with default tags

Initial Replica Set

The initial replica set is the first deployment of domain controllers and is required. It inherits the managed domain's location.

initial_replica_set = {
    subnet = "hsw.aadds" # Required — key from the subnet variable (dot notation: <vnet_key>.<subnet_key>)
}
Field Type Required Description
subnet string Yes Key of the dedicated subnet entry for the domain controllers. The module resolves the subnet ID internally

Security

Controls password-hash synchronization and legacy protocol support.

security = {
    sync_kerberos_passwords = true
    sync_ntlm_passwords     = true
    tls_v1_enabled          = false
}
Field Type Required Default Description
kerberos_armoring_enabled bool No false Enable Kerberos Armoring
kerberos_rc4_encryption_enabled bool No false Enable Kerberos RC4 encryption
ntlm_v1_enabled bool No false Enable legacy NTLM v1 support (leave disabled for security)
sync_kerberos_passwords bool No true Synchronize Kerberos password hashes
sync_ntlm_passwords bool No true Synchronize NTLM password hashes
sync_on_prem_passwords bool No false Synchronize on-premises password hashes (enable only for hybrid/Entra Connect)
tls_v1_enabled bool No false Enable legacy TLS v1. Leave false — setting true causes the platform to reject creation

Notifications

notifications = {
    additional_recipients = ["[email protected]"]
    notify_dc_admins      = true
    notify_global_admins  = false
}
Field Type Required Default Description
additional_recipients list(string) No [] Additional email addresses to notify on managed-domain alerts
notify_dc_admins bool No true Notify members of the AAD DC Administrators group
notify_global_admins bool No false Notify all Global Administrators

Secure LDAP

Enables LDAPS access to the managed domain. Omitted by default. Avoid enabling external_access_enabled unless internet-facing LDAPS is explicitly required.

secure_ldap = {
    enabled                  = true
    external_access_enabled  = false
    pfx_certificate          = "<base64-encoded-pfx>"
    pfx_certificate_password = "<pfx-password>"
}
Field Type Required Default Description
enabled bool Yes - Whether to enable secure LDAP
external_access_enabled bool No false Whether to expose LDAPS over the internet
pfx_certificate string Yes - Base64-encoded TripleDES-SHA1 encrypted PKCS#12 (PFX) bundle
pfx_certificate_password string Yes - Password to decrypt the PFX bundle

Referencing an Existing Domain

Because the managed domain is a tenant singleton, it often already exists. Set existing = true to reference it through a data source instead of creating it. Only name (resolved from the naming convention or set explicitly) and resource_group are needed in that case:

active_directory_domain_service = {
    aadds = {
        existing       = true
        name           = "aadds.sapphirehealth.org"
        resource_group = "hsw"
    }
}

The module merges resource and data outputs, so downstream references work the same regardless of which path created the object.

Naming Convention

The managed domain display name is automatically generated using the following pattern:

{name_prefix}active_directory_domain_service{key}{name_suffix}active_directory_domain_service

For example, with the following prefixes and suffixes:

name_prefixes = {
    active_directory_domain_service = "prod-"
}

name_suffixes = {
    active_directory_domain_service = "-eastus2-aadds"
}

active_directory_domain_service = {
    aadds = {
        resource_group = "hsw"
        domain_name    = "aadds.sapphirehealth.org"
        initial_replica_set = {
            subnet = "hsw.aadds"
        }
    }
}

The resulting resource display name would be: prod-aadds-eastus2-aadds

Note: The display name is the ARM resource name and is independent of domain_name (the DNS namespace). To override automatic naming, specify a custom name.