A
Arun's Blog
← Back to all posts

Automating a Two-Tier PKI Infrastructure with Terraform and Ansible

AutomationPKITerraform
TL;DR

Fully automate a Windows two-tier PKI deployment on AWS using Terraform for infrastructure provisioning and Ansible for certificate workflow. Deploy a Domain Controller, Standalone Root CA, and Standalone Subordinate CA in ~22 minutes. Key insight: use -f -Silent flags with certutil commands and certutil -view instead of certreq -Retrieve for reliable WinRM automation.

Introduction

Building a Public Key Infrastructure (PKI) from scratch involves many manual steps - provisioning servers, configuring Active Directory, installing Certificate Services, and managing the certificate signing workflow between CAs. This post walks through how we fully automated a two-tier PKI deployment on AWS using Terraform and Ansible.

Architecture Overview

Our infrastructure consists of three Windows Server 2019 instances:

  • DC01 - Active Directory Domain Controller
  • RootCA - Standalone Root Certificate Authority (offline-capable)
  • Sub01 - Standalone Subordinate CA (Issuing CA)
PKI Architecture Diagram
Two-tier PKI architecture on AWS

The Automation Pipeline

The deployment follows a two-phase approach:

  1. Terraform - Provisions AWS infrastructure and bootstraps instances
  2. Ansible - Configures the PKI hierarchy and certificate workflow
Automation Pipeline Diagram
Terraform and Ansible automation pipeline

Terraform Configuration

Terraform handles the AWS infrastructure provisioning. Key resources include:

Network Infrastructure

resource "aws_vpc" "main" {
  cidr_block           = "10.10.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
}

resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.10.1.0/24"
  map_public_ip_on_launch = true
}

EC2 Instances with UserData

Each instance uses a PowerShell userdata script for initial configuration:

resource "aws_instance" "domain_controller" {
  ami           = data.aws_ami.windows_2019.id
  instance_type = "t3.medium"
  user_data     = local.userdata_dc
  # ...
}

The DC userdata script handles AD DS installation and domain promotion:

# Install AD DS
Install-WindowsFeature AD-Domain-Services -IncludeManagementTools

# Promote to Domain Controller
Install-ADDSForest `
  -DomainName "corp.itarundaniel.com" `
  -SafeModeAdministratorPassword $securePassword `
  -Force

Dynamic Inventory Generation

Terraform generates the Ansible inventory dynamically:

resource "local_file" "ansible_inventory" {
  content = <<-EOF
    [domain_controller]
    ${aws_instance.domain_controller.public_ip}

    [rootca]
    ${aws_instance.rootca.public_ip}

    [sub01]
    ${aws_instance.sub01.public_ip}

    [all:vars]
    dc_private_ip=${aws_instance.domain_controller.private_ip}
    # ...
  EOF
  filename = "./ansible/inventory.ini"
}

Ansible Playbook Structure

The Ansible playbook orchestrates the PKI configuration through 9 plays:

PKI Certificate Workflow
Certificate signing workflow between Root CA and Sub CA

Key Ansible Tasks

Installing the Root CA:

- name: Configure Standalone Root CA
  ansible.windows.win_shell: |
    Install-ADcsCertificationAuthority `
      -CACommonName "RootCA01" `
      -CAType StandaloneRootCA `
      -CryptoProviderName "RSA#Microsoft Software Key Storage Provider" `
      -HashAlgorithmName SHA256 `
      -KeyLength 2048 `
      -ValidityPeriod Years `
      -ValidityPeriodUnits 20 `
      -Force

Installing the Subordinate CA:

- name: Install Standalone Subordinate CA
  ansible.windows.win_shell: |
    Install-ADcsCertificationAuthority `
      -CACommonName "IssuingCA" `
      -CAType StandaloneSubordinateCA `
      -OutputCertRequestFile "C:\temp\subca.req" `
      -OverwriteExistingKey `
      -Force

Signing the Sub CA certificate:

- name: Resubmit and issue the certificate
  ansible.windows.win_shell: |
    $requestId = (Get-Content "C:/temp/subca_req/request_id.txt" -Raw).Trim()
    certutil -resubmit $requestId

Challenges & Solutions

Challenge 1: certutil -installcert Hangs via WinRM

Problem: The certutil -installcert command hangs indefinitely when executed via WinRM/Ansible because it waits for interactive confirmation.

Solution: Use the -f (force) and -Silent flags:

- name: Install the signed certificate
  ansible.windows.win_shell: |
    certutil -f -Silent -installcert "C:/temp/ca_files/subca.crt"
Pro Tip

Always use -f -Silent with certutil commands in automation. Without these flags, certutil prompts for user confirmation which causes WinRM sessions to hang indefinitely.

Challenge 2: certreq -Retrieve Also Hangs

Problem: Similar to installcert, certreq -Retrieve hangs via WinRM.

Solution: Use certutil -view to extract the certificate instead:

- name: Retrieve certificate
  ansible.windows.win_shell: |
    certutil -config "rootca\RootCA01" -view `
      -restrict "RequestID=$requestId" `
      -out RawCertificate | Out-File subca_raw.txt

    # Extract PEM certificate from output
    $raw = Get-Content subca_raw.txt -Raw
    $match = [regex]::Match($raw, "-----BEGIN CERTIFICATE-----.+-----END CERTIFICATE-----")
    $match.Value | Out-File subca.crt -Encoding ASCII
Important

The certreq -Retrieve command is designed for interactive use. For automation, always use certutil -view with the -out RawCertificate flag to extract certificates non-interactively.

Challenge 3: Enterprise CA vs Standalone CA

Problem: Enterprise Subordinate CA requires AD DS integration and can fail with ERROR_DS_RANGE_CONSTRAINT errors.

Solution: Use Standalone Subordinate CA instead:

# Instead of EnterpriseSubordinateCA
-CAType StandaloneSubordinateCA

Challenge 4: NTLM Credential Delegation

Problem: Commands like certutil -dspublish fail via WinRM due to NTLM not supporting credential delegation to LDAP.

Solution: Run AD-dependent commands from the DC itself, or add the Root CA certificate to trusted roots locally instead of publishing to AD.

Final Result

After running terraform apply, the entire infrastructure is deployed and configured automatically in approximately 22 minutes:

PLAY RECAP *******************************************************************
dc01       : ok=6    changed=1    failed=0    ignored=1
rootca     : ok=24   changed=20   failed=0    ignored=0
sub01      : ok=29   changed=22   failed=0    ignored=2

Finished at Sun Feb  1 07:22:34 PM EST 2026

The Sub CA is fully operational:

Sub CA is running successfully
CA name: IssuingCA
CA type: 4 -- Stand-alone Subordinate CA
CA cert[0]: 3 -- Valid
CRL[0]: 3 -- Valid
DNS Name: sub01.corp.itarundaniel.com

Repository Structure

Repository Structure
Project file organization

Conclusion

Automating PKI deployment with Terraform and Ansible provides:

  • Repeatability - Consistent deployments every time
  • Speed - Full PKI in ~22 minutes vs hours manually
  • Documentation as Code - Infrastructure is self-documenting
  • Version Control - Track changes over time

The key lessons learned:

  1. Windows Certificate Services commands often require special flags (-f, -Silent) for non-interactive execution
  2. WinRM has limitations with credential delegation - plan accordingly
  3. Standalone CAs are easier to automate than Enterprise CAs
  4. Using certutil -view is more reliable than certreq -Retrieve for automation
Note

This infrastructure serves as the foundation for AWS IAM Roles Anywhere, enabling on-premises workloads to obtain temporary AWS credentials using X.509 certificates issued by this PKI.