Testing Self-Managed AD Domain Join
This guide provides a complete checklist for setting up and testing self-managed AD domain join functionality in a lab or development environment.
Prerequisites Checklist
Use this checklist to ensure all components are properly configured before attempting domain join testing.
1. Deploy Domain Controller
- [ ] Deploy Windows Server instance (2019 or 2022)
- [ ] Assign a static private IP address (not DHCP)
- [ ] Configure security group to allow AD ports (see Security Groups)
- [ ] Verify SSM agent is running and instance appears in Fleet Manager
Static IP Requirement
Domain controllers must have static IPs. If using DHCP initially, configure a static IP before promotion or create a DHCP reservation.
2. Promote Domain Controller
Install AD DS role and promote to domain controller:
# Install AD DS role
Install-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools
# Promote to domain controller (creates new forest)
Install-ADDSForest `
-DomainName "sapphy.int" `
-DomainNetbiosName "SAPPHY" `
-SafeModeAdministratorPassword (ConvertTo-SecureString "YourSafeModePassword123!" -AsPlainText -Force) `
-InstallDns `
-Force
Automatic Reboot
The server will automatically reboot after promotion completes. Wait 5-10 minutes for all AD services to start.
3. Create Service Account
After DC promotion, create a dedicated service account for domain join operations:
# Create service account
New-ADUser -Name "svc-domainjoin" `
-UserPrincipalName "[email protected]" `
-SamAccountName "svc-domainjoin" `
-AccountPassword (ConvertTo-SecureString "ServicePassword123!" -AsPlainText -Force) `
-Enabled $true `
-PasswordNeverExpires $true `
-CannotChangePassword $true
# Grant domain join permissions
# Option 1: Add to Domain Admins (for testing only)
Add-ADGroupMember -Identity "Domain Admins" -Members "svc-domainjoin"
# Option 2: Delegate specific permissions (production)
# Use Active Directory Delegation Wizard to grant "Join a computer to the domain"
Production vs Testing
For testing, adding to Domain Admins is acceptable. For production, use least-privilege delegation with specific "Join computer to domain" rights.
4. Create Organizational Unit (Optional)
Create a dedicated OU for AWS-joined computers:
# Create OU structure
New-ADOrganizationalUnit -Name "Servers" -Path "DC=sapphy,DC=int"
New-ADOrganizationalUnit -Name "AWS" -Path "OU=Servers,DC=sapphy,DC=int"
# Verify OU creation
Get-ADOrganizationalUnit -Filter 'Name -like "AWS"' | Select-Object DistinguishedName
Expected Output:
Delegate OU Permissions (Production)
# Grant service account permission to create computer objects in the OU
# This must be done via GUI: Active Directory Users and Computers
# 1. Right-click OU → Delegate Control
# 2. Add "svc-domainjoin"
# 3. Select "Create, delete, and manage user accounts" and "Create, delete, and manage computer objects"
5. Create Secret in AWS Secrets Manager
Create a secret containing the service account credentials:
Via AWS Console:
1. Navigate to Secrets Manager → Store a new secret
2. Select Other type of secret
3. Key/value pairs:
- Key: username, Value: SAPPHY\svc-domainjoin
- Key: password, Value: ServicePassword123!
4. Name: sapphy/domainjoin
5. Note the ARN (needed for Terraform)
Via AWS CLI:
aws secretsmanager create-secret \
--name "sapphy/domainjoin" \
--description "Domain join credentials for self-managed AD" \
--secret-string '{"username":"SAPPHY\\svc-domainjoin","password":"ServicePassword123!"}' \
--region us-east-2
Username Format
Use DOMAIN\username format with escaped backslash in JSON: SAPPHY\\svc-domainjoin
6. Configure DNS Resolution
Instances must be able to resolve the domain name to join it. Choose one of these options:
Option A: DHCP Option Sets (Simpler)
# In your VPC configuration
resource "aws_vpc_dhcp_options" "domain_dns" {
domain_name = "sapphy.int"
domain_name_servers = ["10.248.17.4"] # DC private IP
tags = {
Name = "Domain-DNS-Options"
}
}
resource "aws_vpc_dhcp_options_association" "dns_association" {
vpc_id = aws_vpc.main.id
dhcp_options_id = aws_vpc_dhcp_options.domain_dns.id
}
Option B: Route 53 Resolver Rules
7. Configure IAM Policies and Roles
Create instance profile with required permissions:
# IAM policy for SSM and Secrets Manager
resource "aws_iam_policy" "domain_join" {
name = "DomainJoinPolicy"
description = "Allows EC2 instances to join self-managed AD"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue"
]
Resource = "arn:aws:secretsmanager:us-east-2:271851283454:secret:sapphy/domainjoin-*"
}
]
})
}
# IAM role for EC2 instances
resource "aws_iam_role" "domain_member" {
name = "DomainMemberRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
}
# Attach managed SSM policy
resource "aws_iam_role_policy_attachment" "ssm_managed" {
role = aws_iam_role.domain_member.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# Attach custom domain join policy
resource "aws_iam_role_policy_attachment" "domain_join" {
role = aws_iam_role.domain_member.name
policy_arn = aws_iam_policy.domain_join.arn
}
# Create instance profile
resource "aws_iam_instance_profile" "domain_member" {
name = "DomainMemberInstanceProfile"
role = aws_iam_role.domain_member.name
}
8. Configure Security Groups {#security-groups}
Domain Controller Security Group
security_groups = {
DomainControllerSG = {
ingress = {
DNS_TCP = {
from_port = 53
to_port = 53
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/8" # Adjust to your VPC CIDR
}
DNS_UDP = {
from_port = 53
to_port = 53
ip_protocol = "udp"
cidr_ipv4 = "10.0.0.0/8"
}
Kerberos_TCP = {
from_port = 88
to_port = 88
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/8"
}
Kerberos_UDP = {
from_port = 88
to_port = 88
ip_protocol = "udp"
cidr_ipv4 = "10.0.0.0/8"
}
RPC = {
from_port = 135
to_port = 135
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/8"
}
LDAP_TCP = {
from_port = 389
to_port = 389
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/8"
}
LDAP_UDP = {
from_port = 389
to_port = 389
ip_protocol = "udp"
cidr_ipv4 = "10.0.0.0/8"
}
SMB = {
from_port = 445
to_port = 445
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/8"
}
LDAPS = {
from_port = 636
to_port = 636
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/8"
}
GlobalCatalog = {
from_port = 3268
to_port = 3269
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/8"
}
RPC_Dynamic = {
from_port = 49152
to_port = 65535
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/8"
}
RDP = {
from_port = 3389
to_port = 3389
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/0" # Restrict to your IP
}
}
egress = {
Allow_All = {
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
}
}
}
Target Instance Security Group
security_groups = {
DomainMemberSG = {
ingress = {
RDP = {
from_port = 3389
to_port = 3389
ip_protocol = "tcp"
cidr_ipv4 = "10.0.0.0/0" # Restrict to your IP
}
}
egress = {
Allow_All = {
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
}
}
}
9. SSM Prerequisites
Ensure target instances can communicate with SSM:
Option A: VPC Endpoints (for private subnets without NAT)
endpoints = {
ssm = {
service_name = "com.amazonaws.us-east-2.ssm"
vpc_endpoint_type = "Interface"
subnets = ["PrivateSubnet1", "PrivateSubnet2"]
security_groups = ["EndpointSG"]
}
ssmmessages = {
service_name = "com.amazonaws.us-east-2.ssmmessages"
vpc_endpoint_type = "Interface"
subnets = ["PrivateSubnet1", "PrivateSubnet2"]
security_groups = ["EndpointSG"]
}
ec2messages = {
service_name = "com.amazonaws.us-east-2.ec2messages"
vpc_endpoint_type = "Interface"
subnets = ["PrivateSubnet1", "PrivateSubnet2"]
security_groups = ["EndpointSG"]
}
}
Option B: NAT Gateway/Internet Gateway (simpler for testing) - Ensure subnet route table has route to NAT Gateway or IGW - Instance can reach SSM endpoints via internet
10. Reference Secret in Terraform Variables
Update your tfvars file:
# Reference the secret
secretsmanager_secrets = {
domain_join_creds = {
arn = "arn:aws:secretsmanager:us-east-2:271851283454:secret:sapphy/domainjoin-Z9EFWO"
}
}
# Define domain join document
domain_join = {
SelfManaged_AD_Domain_Join = {
ad_type = "self-managed"
content = {
mainSteps = [{
inputs = {
directoryName = "sapphy.int"
dnsIpAddresses = ["10.248.17.4"] # DC private IP
secretArn = "arn:aws:secretsmanager:us-east-2:271851283454:secret:sapphy/domainjoin-Z9EFWO"
organizationalUnit = "OU=AWS,OU=Servers,DC=sapphy,DC=int" # Optional
}
}]
}
tags = {
Environment = "nonprod"
Purpose = "Self-Managed AD Domain Join Testing"
}
}
}
11. Deploy Target Instance
Configure an instance to be domain-joined:
instances = {
test = {
names = ["TestServer01"]
instance_type = "t3.medium"
iam_instance_profile = "EC2InstanceProfile"
key_pair = "gianniIRE"
domain_join = "SelfManaged_AD_Domain_Join"
nics = {
primary = {
subnet = "SharedInfra.SharedInfraPrivateAZ1"
security_groups = ["DomainMemberSG"]
}
}
}
}
Deploy with Terraform:
12. Monitor Domain Join Process
Check SSM Command History
- Navigate to AWS Systems Manager → Run Command
- Find the association execution for your instance
- Click on the command ID to view output
- Check for PowerShell errors in the output
View CloudWatch Logs
SSM agent logs to CloudWatch (if configured):
- Log group: /aws/ssm/instance-id
- Look for domain join execution output
Common Success Indicators
In SSM output:
After reboot (RDP to instance):
# Verify domain membership
(Get-WmiObject -Class Win32_ComputerSystem).Domain
# Expected: sapphy.int
# Check computer account
whoami
# Expected: SAPPHY\TESTSERVER01$
13. Verify in Active Directory
On the domain controller:
# Check if computer account exists
Get-ADComputer -Filter 'Name -like "TestServer*"' | Select-Object Name, DistinguishedName
# Verify OU placement (if specified)
Get-ADComputer -Filter 'Name -like "TestServer*"' | Select-Object DistinguishedName
# Expected: CN=TestServer01,OU=AWS,OU=Servers,DC=sapphy,DC=int
Common Issues and Solutions
Domain Join Fails - "The specified domain either does not exist or could not be contacted"
Cause: DNS resolution failure
Solutions:
1. Verify DC private IP in dnsIpAddresses
2. Check DHCP option set or Route 53 resolver rules
3. Test DNS from instance:
Domain Join Fails - "Access is denied"
Cause: Invalid credentials or insufficient permissions
Solutions:
1. Verify secret contains correct username/password
2. Check username format: DOMAIN\username (with escaped backslash in JSON)
3. Ensure service account can join computers to domain
4. Check computer account quota (default 10 per user)
SSM Document Not Executing
Cause: SSM connectivity issues
Solutions:
1. Verify instance appears in Fleet Manager
2. Check IAM instance profile has AmazonSSMManagedInstanceCore
3. Ensure VPC endpoints or NAT Gateway configured
4. Check security groups allow HTTPS outbound
Computer Not in Specified OU
Cause: OU doesn't exist or service account lacks permissions
Solutions: 1. Verify OU Distinguished Name is correct 2. Ensure OU exists before domain join 3. Grant service account "Create Computer Objects" in target OU 4. Test without OU first, then add OU parameter
Testing Workflow Summary
graph TD
A[Deploy DC Instance] --> B[Promote to DC]
B --> C[Create Service Account]
C --> D[Create OU Optional]
D --> E[Create AWS Secret]
E --> F[Configure DNS]
F --> G[Configure IAM]
G --> H[Configure Security Groups]
H --> I[Deploy Target Instance]
I --> J[Monitor SSM Execution]
J --> K[Verify Domain Membership]
K --> L{Success?}
L -->|Yes| M[Complete]
L -->|No| N[Troubleshoot]
N --> J
Cleanup
To remove test resources:
# Destroy target instance
terraform -chdir=src destroy -target=module.instance.aws_instance.instance[\"TestServer01\"] -var-file="../environments/nonprod/gianni_ire.tfvars"
# Manually delete secret (with recovery window)
aws secretsmanager delete-secret --secret-id sapphy/domainjoin --recovery-window-in-days 7
# Destroy DC (if no longer needed)
terraform -chdir=src destroy -target=module.instance.aws_instance.instance[\"DomainController01\"] -var-file="../environments/nonprod/gianni_ire.tfvars"
Next Steps
After successful testing, consider: - Implementing credential rotation in Secrets Manager - Creating separate service accounts per environment - Documenting customer-specific OU structure requirements - Setting up CloudWatch alarms for failed domain joins - Creating runbooks for troubleshooting common issues