How a disciplined audit — evidence first, fix second, verify third — found and closed the real security gaps behind a working Azure domain controller: weak IPsec crypto, a 16,384-port firewall grant, a VPN CA key sitting on a workstation, and privileged accounts out of scope. An AI agent did the hunting and the proving; a human approved every privileged change — and the most sensitive one stays in human hands to apply.
Companion to the "Deploying a Domain Controller to Azure" case study. That project built the Azure DC and proved it replicates. This one secures it.
The first case study ended with a working, replicating domain controller in Azure. But "working" is not "secure." The tunnel that makes the Azure DC useful is also its only front door, and a quick look at the live state showed four gaps that a customer or a regulator would immediately notice:
| # | Gap (as found) | Severity |
|---|---|---|
| W1 | The IPsec tunnel was negotiating AES-128-CBC / SHA-1 / MODP-1024 — an audit-visible weak combination, in a tunnel that is the only path to a domain controller | High |
| W2 | The NSG allowed 16,384 inbound RPC/DFSR/DNS ports to the DC (range 49152–65535) | Medium |
| W3 | The point-to-site VPN root-CA private key — and the client certificate — sat as plaintext files on a workstation: a single point of failure for the entire VPN identity | Medium |
| W4 | The Windows baseline of the DC itself had never been audited: privileged groups, local accounts, RDP group, services | Medium |
The working rule for this whole track was the same as the build: evidence before assertion, a state change only after an explicit human approval, and secrets handled as references — never values.
Every finding followed the same loop: capture the live state as evidence → propose the smallest change that closes the gap → apply only after approval → verify the after-state with the same tool that produced the before-state. Where a tool could not see in (a service-account context, a locked file, a kernel counter), the gap was recorded as inconclusive — never quietly read as "compliant."
The tunnel reported healthy, connected, and moving data on both ends. But its own status output showed the negotiated proposal: AES_CBC_128 / HMAC_SHA1 / MODP_1024. The cause was subtle: the gateway had no explicit IPsec policy, so it had fallen back to a default set that included the weak pair. No error, no warning — just a weak handshake in a log nobody was reading.
The fix was a two-sided pin, chosen to the strongest combination the gateway class actually supports (the DH-group ceiling was checked against the live --help, not assumed): AES-256 / SHA-384 / ECP-384 in the IKE phase and AES-256-GCM in the ESP phase on both Azure and strongSwan.
This is where the verification discipline paid off, in the form of a mistake the agent itself made and caught. The first rekey dropped the tunnel: the IKE phase was pinned correctly but the ESP line was missing its ECP group, so on the first rekey Azure offered GCM over ECP-384 while the local side offered GCM over MODP-2048 — no common proposal, and the tunnel silently died. The signature looked exactly like a network outage. It was root-caused to that one token, corrected to …-ecp384, and re-keyed. The lesson recorded: an "IKE up, ESP dead" state is indistinguishable from a tunnel outage unless you read both phases — and a rekey failure is not proof the fix was wrong, only proof something is still mismatched.
The NSG rule was 49152–65535 — a blanket 16,384-port allow to a domain controller. The audit did not guess at a "safe range"; it read the actual live listeners on the box and found the real set was far smaller: RPC endpoint mapper on 135, the LSASS LDAP endpoints in 49664–49704, ADWS, and the two service ports for DFSR and DNS.
The change was the smallest one that closed the gap with zero Windows-state impact: narrow the NSG to exactly the observed listener set. No service restart, no LDAP rebind, fully reversible with a single command.
Before → after: 16,384 allowed inbound RPC/DFSR/DNS ports → 43 (41 in 49664–49704, plus DFSR and DNS), all still scoped to the single trusted source.
Verified by a port matrix, not a claim: in-range ports (389, 135, 49664, 53281, 56715) open, and previously-allowed out-of-range ports (49153, 50000, 60000) now filtered — the hardening is visible, not assumed. AD replication, the function that actually depends on those ports, stayed green through and after the change.
The gateway held only the root certificate; the private key was needed only to sign a new client certificate. But that key — plus the client key and certificate — was sitting as plaintext files on a workstation. A single lost or stolen workstation was a total compromise of the VPN identity.
The fix moved all four files into a local, encrypted vault (gpg, AES-256, PBKDF2), with a passphrase generated and shown to the human exactly once and never recorded. Every file was round-trip verified (decrypt, hash-match, size-match), then the plaintext originals — including a stray duplicate — were shredded and a full-disk sweep confirmed no P2S private key remained anywhere on the host.
Verified: the decrypted root key's public half matches the certificate already installed on the gateway — so the vault holds the correct key, not a copy of something else.
This is the audit that found the most, because it was the least looked at. A read-only sweep of the live DC surfaced:
The agent remediated the High items it could reach in-context and verified each with the same command that produced the finding:
H1 Guest account -> disabled (verified: Enabled=False) H2 Print Spooler -> Stopped, Disabled (verified: State/StartType) H3 3 human-named local SAM -> disabled (verified: Enabled=False each)
During remediation, a transient state reading briefly suggested the three accounts might have re-enabled themselves — a discrepancy serious enough to warrant a full stop and independent investigation before proceeding further. A dedicated audit-log review found zero account-enable events across the entire window, from any source, and traced the only account-modification activity to the remediation script's own service context. A second, independent re-verification twenty-four hours later — fresh event log query, fresh account-state read — confirmed the same result: all three accounts remain disabled, and no enable event exists anywhere in the log. The original reading is best explained as a transient capture artifact, not a real state change or unauthorized action. The finding: closed, with two independent confirmations rather than one.
The Medium item of a privileged service account with excess rights was identified, scoped for reduction, and staged and handed to the operator to apply over an interactive RDP session — the kind of change to a production AD identity that the operating model deliberately keeps in human hands (see below). A few lines — Group Policy root cause, Defender signature currency, SMB signing, TLS minimum version — sit behind an elevated interactive session the audit context could not read; they are reported as inconclusive, not "compliant."
A security fix that vanishes on reboot is not a fix. The on-prem host is on a nightly power schedule, so the real test was whether the whole secured stack — tunnel, firewall order, replication — came back up on its own after a full power cycle, unattended.
It did, and it had already survived one: a prior reboot had regenerated one of the firewall chains empty, breaking one direction of AD replication with a single, direction-specific error. The agent's recovery script was rewritten to verify the rule's position and correctness, not just its presence, so the next reboot did not reproduce the failure. On the following power cycle, the tunnel auto-reconnected within seconds, the firewall rules were re-pinned at the correct chain positions, and AD replication was green in both directions across all three domain controllers with no manual intervention.
The build case study's rule held, and got stricter, because this track is privilege-reducing rather than build-out:
The decisive check — the same command the build used, re-run after the audit — is unchanged and green. Verbatim from the audit window (ops/w4-D-out.txt):
repadmin /replsummary Source DSA largest delta fails/total %% error CONTO-DC 12m:56s 0 / 10 0 CONTO-DC2 12m:56s 0 / 10 0 vm-w2022-poc 02m:38s 0 / 10 0 Destination DSA largest delta fails/total %% error CONTO-DC 02m:39s 0 / 10 0 CONTO-DC2 :51s 0 / 10 0 vm-w2022-poc 12m:58s 0 / 10 0
Zero replication failures, in both directions, across all three domain controllers. The same check was re-run live after the subsequent power cycle and security changes, and returned the same result — zero failures in both directions. The tunnel was on the hardened AES-256/GCM/ECP-384 policy, the NSG narrowed to the observed listener set, the VPN CA key off-host, and the DC's High-severity local-account and service findings closed.
The build proved an AI agent can do hard systems work end-to-end. The audit adds the part that's more valuable in practice: the agent can find the gaps a working system hides, and — more importantly — prove which of its own fixes are real. It caught its own crypto pinning mistake from the rekey failure, distinguished a "tunnel down" from an "ESP mismatch," treated a self-contradicting piece of evidence as a reason to stop and dig deeper rather than explain away, refused to read a tool it couldn't see through as "compliant," and left every AD-identity decision to the human.
That boundary — evidence before assertion, approval before privilege, and "inconclusive" left honestly inconclusive — is the part of the model that a customer can actually rely on.