Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Azure Active Directory Domain Controller — Terraform Deployment

Deploys a Windows Server 2022 Active Directory Domain Controller to Azure using Terraform, configured the way a production environment would be: secrets in Key Vault, a system-assigned managed identity for credential-free auth, no public IP on the domain controller, and access via Azure Bastion.

This is a hands-on portfolio lab. The headline isn't "a DC got deployed" — it's the production access and secrets patterns around it, and an honest account of which corners were deliberately cut for a single-day lab and how they'd be uncut in production.

📹 Video walkthrough — a guided tour of each file, the design decisions, and what would change in a real enterprise environment.


Architecture

graph TB
    subgraph Internet
        Admin[Admin Workstation]
    end

    subgraph "Azure — rg-ad-eli (southafricanorth)"
        subgraph KV["Key Vault (RBAC-enabled)"]
            S1[vm-admin-password]
            S2[dsrm-password]
        end

        subgraph VNet["vnet-ad-eli — 10.0.0.0/16"]
            subgraph BSub["AzureBastionSubnet — 10.0.2.0/26"]
                Bastion[Azure Bastion<br/>Standard SKU]
            end
            subgraph ADSub["snet-ad — 10.0.1.0/24"]
                NSG[NSG: RDP from VNet + mgmt IP only]
                VM[vm-ad-eli<br/>DC + DNS<br/>10.0.1.4<br/>Managed Identity]
            end
        end
    end

    Admin -->|TLS / browser RDP| Bastion
    Bastion -->|RDP over VNet| VM
    VM -->|Managed Identity<br/>Secrets User| KV
    Admin -.->|az CLI<br/>Secrets Officer| KV

    style VM fill:#0078d4,color:#fff
    style Bastion fill:#107c10,color:#fff
    style KV fill:#5c2d91,color:#fff
Loading

Flow: Admin connects through Bastion over TLS — the DC has no public IP. The VM authenticates to Key Vault using its own managed identity (read-only on secrets). The admin populates secrets out-of-band via the Azure CLI (read/write). Terraform state lives in a remote, encrypted Azure Storage backend.


What gets deployed

Resource Name Notes
Resource group rg-ad-eli Everything nests here; single destroy cleans up
Virtual network vnet-ad-eli 10.0.0.0/16
AD subnet snet-ad 10.0.1.0/24; DC at static 10.0.1.4
Bastion subnet AzureBastionSubnet 10.0.2.0/26; name is mandatory, /26 minimum
Key Vault kv-adlab-eli-01 RBAC authorization; holds both secrets
NSG nsg-ad-eli RDP from mgmt IP + VirtualNetwork tag only
NIC nic-ad-eli No public IP
VM vm-ad-eli Windows Server 2022, Standard_DS1_v2, system-assigned identity
Bastion bastion-ad-eli Standard SKU, own public IP
Extension install-ad-ds Installs AD DS, promotes new forest corp.eli.com

Key design decisions

Managed identity over passwords-in-code. The VM gets a system-assigned managed identity that's granted Key Vault Secrets User. The password variables most tutorials keep in variables.tf were removed entirely — a variable is just one more place a secret can leak.

RBAC over access policies on Key Vault. rbac_authorization_enabled = true puts vault permissions on the same auditable control plane as the rest of Azure. Two scoped, least-privilege assignments: the admin gets Secrets Officer (read/write), the VM identity gets Secrets User (read-only).

Data-plane vs. control-plane roles. Secrets Officer/Secrets User act on the secrets inside the vault — distinct from Contributor, which manages the vault resource. Managing a vault and reading its secrets are separate grants.

No public IP on the domain controller. The DC is internet-unreachable. Access is via Bastion over TLS, so the only public-facing surface is a managed, patched jump host — never an open 3389 on a DC.

Remote, encrypted Terraform state. State holds secrets in cleartext, so it lives in an access-controlled Azure Storage backend, never on disk. Each lab uses a distinct state key in shared state storage.

Staged apply for secret bootstrapping. Because the secret data sources read at plan time, the build is staged: (1) targeted apply creates the vault + admin access, (2) secrets set via CLI, (3) full apply reads them and builds everything. Consumers can't read secrets that don't yet exist — provisioning order matters.


Deploy

Prerequisites: Azure CLI (az login), Terraform 1.3+, an Azure subscription, and a remote-state storage account.

# Stage 1 — vault + your access only
terraform init
terraform apply -target="azurerm_key_vault.main" -target="azurerm_role_assignment.kv_admin"

# Stage 2 — set secrets (wait ~60s for RBAC propagation first)
az keyvault secret set --vault-name "kv-adlab-eli-01" --name "vm-admin-password" --value '<strong-password>'
az keyvault secret set --vault-name "kv-adlab-eli-01" --name "dsrm-password"     --value '<strong-password>'

# Stage 3 — full build (~25-30 min; Bastion alone is 10-15)
terraform apply

Connect: Azure Portal → vm-ad-eliConnect → BastionCORP\adadmin + the admin password.

Teardown (same day): terraform destroy.


Evidence

Clean apply — 11 resources, extension completed without error:

terraform apply complete

Domain controller has no public IP — access is Bastion-only:

VM overview, no public IP

Get-ADDomain confirms a healthy forest corp.eli.com (NetBIOS CORP):

Get-ADDomain output

VM system-assigned managed identity:

VM identity blade

Key Vault RBAC — admin as Secrets Officer, VM identity as Secrets User, both scoped to the vault:

Key Vault access control

Provider deprecation caught and remediated, then clean validate:

terraform validate


Troubleshooting & lessons (framed as skills)

Issue Cause Resolution / lesson
Too many command line arguments on -target PowerShell parses the flag differently than bash Quote each address: -target="azurerm_key_vault.main"
403 Forbidden setting secrets right after apply Azure RBAC takes 30s–few min to propagate Wait ~60s and retry — propagation, not a config error
First full apply would fail reading secrets Data sources read at plan time; secrets didn't exist yet Stage the apply — vault first, set secrets, then full apply
enable_rbac_authorization deprecation warning azurerm 4.x renamed the property Updated to rbac_authorization_enabled ahead of the 5.0 removal
Bastion won't deploy into the subnet Subnet must be named exactly AzureBastionSubnet, /26 min Use the literal mandated name and size
Extension may report Failed (didn't here) AD promotion reboots the VM mid-extension Benign — verify with Get-ADDomain, not extension status
Key Vault name collision on rebuild Soft-delete leaves a ghost after destroy purge_soft_delete_on_destroy = true, or az keyvault purge
curl -s hangs at a Uri: prompt curl is an alias for Invoke-WebRequest in PowerShell Use Invoke-RestMethod or curl.exe

Production hardening — deliberately out of scope

These were left out on purpose for a single-day free-tier lab. Knowing the gap and how to close it is the point.

  • Key Vault networking — uses its public endpoint gated by RBAC for lab access. Production: private endpoint + network ACLs, no public reachability.
  • Purge protection — disabled to allow same-day rebuilds. Production: purge_protection_enabled = true.
  • Secrets in state — even sourced from Key Vault, the passwords materialize in Terraform state and the extension settings (Terraform must use the value). Production: runtime secret retrieval via the VM's managed identity inside a bootstrap script, so the value never enters state. The remote encrypted backend mitigates the at-rest exposure in the meantime.
  • Observability — no diagnostics here. Production: VM and Key Vault diagnostic settings shipped to a Log Analytics workspace for audit and alerting.
  • Resilience — a single forest-root DC is a lab. Production: multiple DCs across availability zones with a proper AD site topology.

Files

.
├── main.tf                    # All resources + AD DS extension
├── variables.tf               # Inputs (no secrets)
├── outputs.tf                 # Names, IDs, access info (no secrets)
├── terraform.tfvars           # Real values (git-ignored)
├── terraform.tfvars.example   # Template (committed)
├── .gitignore                 # Excludes tfvars + state
└── screenshots/               # Deployment evidence

About

Windows Server 2022 AD domain controller on Azure via Terraform : managed identity, Key Vault RBAC, Bastion, zero public IP

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages