From 8263c4d887fbc57bb059dc046d65901112344d82 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Tue, 18 Aug 2026 14:28:42 -0400 Subject: [PATCH 1/6] Expand Requirements with WinRM setup, module checks, and permission split Adds explicit Enable-PSRemoting/firewall/TrustedHosts commands, the DnsServer module verification/reinstall commands, PowerShell version requirements on both the DNS server and gateway host, and separates the two distinct permission concerns (WinRM session access vs. DNS record management) that fail differently and were easy to conflate while troubleshooting. --- docsource/content.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/docsource/content.md b/docsource/content.md index 66a6173..792d688 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -33,9 +33,34 @@ The owning zone for a given FQDN is resolved by listing the server's forward-loo ### Microsoft DNS requirements -1. A Windows Server DNS server (typically a domain controller) hosting the forward-lookup zone(s) for the domains being validated, with the **DNS Server role** installed (this provides the `DnsServer` PowerShell module the plugin invokes on the server). -2. **WinRM (WS-Management) enabled** on the DNS server and reachable from the gateway host (TCP 5985 for HTTP, 5986 for HTTPS). -3. An identity with permission to manage DNS records on the server — either the gateway service account (via Kerberos/Negotiate, when no credentials are configured) or an explicit `AD_Username` / `AD_Password`. The account must be a member of **DnsAdmins** (or Domain Admins) or otherwise delegated DNS record management on the target zone. +1. **A Windows Server DNS server** (typically a domain controller) hosting the forward-lookup zone(s) for the domains being validated, with the **DNS Server role** installed. This is what provides the `DnsServer` PowerShell module (`Add-DnsServerResourceRecord`, `Get-DnsServerResourceRecord`, `Remove-DnsServerResourceRecord`, `Get-DnsServerZone`) that the plugin invokes remotely. Confirm the module is present: + ```powershell + Get-Module -ListAvailable DnsServer + ``` + If it's missing even though the DNS role appears installed, reinstall with management tools included: + ```powershell + Install-WindowsFeature DNS -IncludeManagementTools + ``` + **PowerShell version on the DNS server:** the built-in **Windows PowerShell 5.1** WinRM endpoint (the default remoting endpoint on every supported Windows Server release) is all that's required — this is what ships the `DnsServer` module, and no separate PowerShell 7/pwsh install is needed on the DNS server. If the endpoint has been reconfigured to something non-default (a custom PowerShell 7 remoting endpoint, JEA-constrained endpoint, etc.), confirm the `DnsServer` module is actually importable in that session, since a constrained/alternate endpoint may not expose it. + + **PowerShell on the gateway host:** none required. The plugin doesn't shell out to `powershell.exe`/`pwsh` — it uses the bundled `System.Management.Automation` PowerShell SDK (currently v7.4.6, referenced as a NuGet package in the plugin's `.csproj`) to open the WSMan session in-process. The gateway host only needs the .NET runtime the gateway itself requires (see Runtime Requirements below) and outbound WinRM connectivity. + +2. **WinRM (WS-Management) enabled** on the DNS server, reachable from the gateway host over TCP **5985** (HTTP, default) or **5986** (HTTPS, when `AD_UseSSL=true`). On the DNS server: + ```powershell + Enable-PSRemoting -Force + Set-NetFirewallRule -Name "WINRM-HTTP-In-TCP" -Enabled True + ``` + If the gateway host is **not** domain-joined to the same domain as the DNS server, WinRM's default Negotiate authentication requires the target to be trusted explicitly. On the *gateway* host: + ```powershell + Set-Item WSMan:\localhost\Client\TrustedHosts -Value "" -Concatenate -Force + ``` + `TrustedHosts` disables mutual authentication (the client trusts whatever answers at that address) — acceptable on a private/lab network, but don't wildcard it or point it at anything you don't control. Prefer domain-joined hosts and Kerberos, or `AD_UseSSL=true` with a real cert, where possible. + +3. **Two separate permission concerns** — both are required, and having one without the other produces different failures: + - **WinRM session access**: the identity must be allowed to open a remote PowerShell session on the DNS server at all. This normally requires membership in the local **Remote Management Users** group (or local Administrators) on that server. Missing this fails at the WinRM layer with an `Access is denied` error when opening the session — before the plugin ever gets to run a DNS cmdlet. + - **DNS record management**: the identity must additionally be authorized to manage records on the target zone — membership in **DnsAdmins** (or Domain Admins), or an equivalent delegated ACL on the zone. Missing this lets the WinRM session open successfully, but DNS cmdlets themselves fail with access-denied errors. + + Either the **gateway service account** (via Kerberos/Negotiate, when `AD_Username`/`AD_Password` are left empty — note this is the account the gateway *service* runs as, which may differ from whatever account you're logged in as interactively) or an explicit `AD_Username`/`AD_Password` must satisfy both. ### Configuration fields From 43a409b8b9d75c1195064cbb1d904594d144bc29 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Aug 2026 18:29:27 +0000 Subject: [PATCH 2/6] docs: auto-generate README and documentation [skip ci] --- README.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3becce6..deff392 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,34 @@ The owning zone for a given FQDN is resolved by listing the server's forward-loo ### Microsoft DNS requirements -1. A Windows Server DNS server (typically a domain controller) hosting the forward-lookup zone(s) for the domains being validated, with the **DNS Server role** installed (this provides the `DnsServer` PowerShell module the plugin invokes on the server). -2. **WinRM (WS-Management) enabled** on the DNS server and reachable from the gateway host (TCP 5985 for HTTP, 5986 for HTTPS). -3. An identity with permission to manage DNS records on the server — either the gateway service account (via Kerberos/Negotiate, when no credentials are configured) or an explicit `AD_Username` / `AD_Password`. The account must be a member of **DnsAdmins** (or Domain Admins) or otherwise delegated DNS record management on the target zone. +1. **A Windows Server DNS server** (typically a domain controller) hosting the forward-lookup zone(s) for the domains being validated, with the **DNS Server role** installed. This is what provides the `DnsServer` PowerShell module (`Add-DnsServerResourceRecord`, `Get-DnsServerResourceRecord`, `Remove-DnsServerResourceRecord`, `Get-DnsServerZone`) that the plugin invokes remotely. Confirm the module is present: + ```powershell + Get-Module -ListAvailable DnsServer + ``` + If it's missing even though the DNS role appears installed, reinstall with management tools included: + ```powershell + Install-WindowsFeature DNS -IncludeManagementTools + ``` + **PowerShell version on the DNS server:** the built-in **Windows PowerShell 5.1** WinRM endpoint (the default remoting endpoint on every supported Windows Server release) is all that's required — this is what ships the `DnsServer` module, and no separate PowerShell 7/pwsh install is needed on the DNS server. If the endpoint has been reconfigured to something non-default (a custom PowerShell 7 remoting endpoint, JEA-constrained endpoint, etc.), confirm the `DnsServer` module is actually importable in that session, since a constrained/alternate endpoint may not expose it. + + **PowerShell on the gateway host:** none required. The plugin doesn't shell out to `powershell.exe`/`pwsh` — it uses the bundled `System.Management.Automation` PowerShell SDK (currently v7.4.6, referenced as a NuGet package in the plugin's `.csproj`) to open the WSMan session in-process. The gateway host only needs the .NET runtime the gateway itself requires (see Runtime Requirements below) and outbound WinRM connectivity. + +2. **WinRM (WS-Management) enabled** on the DNS server, reachable from the gateway host over TCP **5985** (HTTP, default) or **5986** (HTTPS, when `AD_UseSSL=true`). On the DNS server: + ```powershell + Enable-PSRemoting -Force + Set-NetFirewallRule -Name "WINRM-HTTP-In-TCP" -Enabled True + ``` + If the gateway host is **not** domain-joined to the same domain as the DNS server, WinRM's default Negotiate authentication requires the target to be trusted explicitly. On the *gateway* host: + ```powershell + Set-Item WSMan:\localhost\Client\TrustedHosts -Value "" -Concatenate -Force + ``` + `TrustedHosts` disables mutual authentication (the client trusts whatever answers at that address) — acceptable on a private/lab network, but don't wildcard it or point it at anything you don't control. Prefer domain-joined hosts and Kerberos, or `AD_UseSSL=true` with a real cert, where possible. + +3. **Two separate permission concerns** — both are required, and having one without the other produces different failures: + - **WinRM session access**: the identity must be allowed to open a remote PowerShell session on the DNS server at all. This normally requires membership in the local **Remote Management Users** group (or local Administrators) on that server. Missing this fails at the WinRM layer with an `Access is denied` error when opening the session — before the plugin ever gets to run a DNS cmdlet. + - **DNS record management**: the identity must additionally be authorized to manage records on the target zone — membership in **DnsAdmins** (or Domain Admins), or an equivalent delegated ACL on the zone. Missing this lets the WinRM session open successfully, but DNS cmdlets themselves fail with access-denied errors. + + Either the **gateway service account** (via Kerberos/Negotiate, when `AD_Username`/`AD_Password` are left empty — note this is the account the gateway *service* runs as, which may differ from whatever account you're logged in as interactively) or an explicit `AD_Username`/`AD_Password` must satisfy both. ### Configuration fields From 30b5176569ae23daef1dab03b622a838bf0e993c Mon Sep 17 00:00:00 2001 From: Brian Hill <76450501+bhillkeyfactor@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:54:10 -0400 Subject: [PATCH 3/6] Update keyfactor-starter-workflow.yml --- .github/workflows/keyfactor-starter-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 1cf6642..4394c8e 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,7 +11,7 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@port-update-catalog-to-v5 + uses: keyfactor/actions/.github/workflows/starter.yml@v5 with: command_token_url: ${{ vars.COMMAND_TOKEN_URL }} From 09e9baa1d98dd0a94e0a78a0ecfd45a621640edf Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Tue, 18 Aug 2026 14:59:31 -0400 Subject: [PATCH 4/6] Use bold lead-ins instead of subheadings for Testing content under Usage Subheadings under Usage weren't rendering through the doc tool's regeneration; trying bold text instead of ### headers to see if that survives the merge. --- docsource/content.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docsource/content.md b/docsource/content.md index 792d688..643f624 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -82,15 +82,13 @@ The owning zone for a given FQDN is resolved by listing the server's forward-loo ## Usage -### Testing - -There are three levels of testing, each isolating a different layer of the stack. See [test/README.md](test/README.md) for full details; summarized here: +**Testing.** There are three levels of testing, each isolating a different layer of the stack. See [test/README.md](test/README.md) for full details; summarized here: 1. **Infra smoke test** (`test/smoke-test.ps1`) — pure PowerShell, no plugin code. Confirms WinRM reachability, the `DnsServer` module, and the target zone from the machine that will host the gateway. 2. **Provider harness** (`test/ManualTestHarness`) — drives `MicrosoftAdDnsProvider` directly (no gateway, no CA). Exercises TXT create/delete, additive multi-value TXT, targeted delete, CNAME create/delete, and idempotent cleanup against a real DNS server. 3. **Full gateway + CA integration** — a real enrollment through the gateway, a CA, and this plugin together. This is the only level that proves the domain validator is wired up correctly end-to-end (gateway config → CA → DNS-01 challenge → this plugin → DNS server → CA re-check → issuance). -### Level 3 against an internal-only zone (e.g. Active Directory `.local` / `.corp`) +**Testing against an internal-only zone (e.g. Active Directory `.local` / `.corp`).** Public ACME CAs (Let's Encrypt, Google Trust Services, etc.) **reject internal/non-public zones outright** — the order fails at `CreateOrder` with `rejectedIdentifier` / `"Domain must end in a public suffix"` before DNS validation is ever attempted, because `.local`-style names aren't ICANN-delegated public suffixes. This is a CA-side policy check, not a DNS or plugin problem, and it means a public CA can never be used to test this plugin against an internal AD zone. From 7bc8f2e17114dae07835e82a7a0808f9df9b6219 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Tue, 18 Aug 2026 15:00:44 -0400 Subject: [PATCH 5/6] =?UTF-8?q?Move=20Testing=20content=20under=20Requirem?= =?UTF-8?q?ents=20=E2=80=94=20Usage=20doesn't=20pass=20through=20the=20doc?= =?UTF-8?q?=20tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against the doc tool's actual regenerated README: only the Overview and Requirements headings from content.md are merged into the generated README, everything else (including Usage) is replaced wholesale by the tool's own boilerplate. Nesting under Requirements instead, since that section is confirmed to pass through verbatim. --- docsource/content.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docsource/content.md b/docsource/content.md index 643f624..d57f949 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -80,17 +80,15 @@ The owning zone for a given FQDN is resolved by listing the server's forward-loo * Each validator type manages only its own record type: `MicrosoftAdDomainValidator` reads/writes `TXT`, `MicrosoftAdCnameDomainValidator` reads/writes `CNAME`. Neither touches other record types. * The DNS server must have the `DnsServer` PowerShell module available (installed with the DNS Server role). -## Usage +### Testing -**Testing.** There are three levels of testing, each isolating a different layer of the stack. See [test/README.md](test/README.md) for full details; summarized here: +There are three levels of testing, each isolating a different layer of the stack. See [test/README.md](test/README.md) for full details; summarized here: 1. **Infra smoke test** (`test/smoke-test.ps1`) — pure PowerShell, no plugin code. Confirms WinRM reachability, the `DnsServer` module, and the target zone from the machine that will host the gateway. 2. **Provider harness** (`test/ManualTestHarness`) — drives `MicrosoftAdDnsProvider` directly (no gateway, no CA). Exercises TXT create/delete, additive multi-value TXT, targeted delete, CNAME create/delete, and idempotent cleanup against a real DNS server. 3. **Full gateway + CA integration** — a real enrollment through the gateway, a CA, and this plugin together. This is the only level that proves the domain validator is wired up correctly end-to-end (gateway config → CA → DNS-01 challenge → this plugin → DNS server → CA re-check → issuance). -**Testing against an internal-only zone (e.g. Active Directory `.local` / `.corp`).** - -Public ACME CAs (Let's Encrypt, Google Trust Services, etc.) **reject internal/non-public zones outright** — the order fails at `CreateOrder` with `rejectedIdentifier` / `"Domain must end in a public suffix"` before DNS validation is ever attempted, because `.local`-style names aren't ICANN-delegated public suffixes. This is a CA-side policy check, not a DNS or plugin problem, and it means a public CA can never be used to test this plugin against an internal AD zone. +Public ACME CAs (Let's Encrypt, Google Trust Services, etc.) **reject internal/non-public zones outright** — the order fails at `CreateOrder` with `rejectedIdentifier` / `"Domain must end in a public suffix"` before DNS validation is ever attempted, because `.local`-style names aren't ICANN-delegated public suffixes. This is a CA-side policy check, not a DNS or plugin problem, and it means a public CA can never be used to test level 3 against an internal AD zone. To test level 3 against an internal zone (e.g. `command.local`), point the gateway's CA connector at a **private ACME server** instead — [step-ca](https://smallstep.com/docs/step-ca/) works well and doesn't enforce public-suffix rules. See [test/README.md](test/README.md#step-3b--full-gateway-integration-against-an-internal-zone-with-a-private-acme-ca-step-ca) for a full step-ca setup and DNS-resolution troubleshooting walkthrough, including two gotchas that are easy to lose time to: From 899154f02ac6aa941a98df19c540d9a63d7b7657 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Aug 2026 19:01:28 +0000 Subject: [PATCH 6/6] docs: auto-generate README and documentation [skip ci] --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index deff392..14c1b95 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,21 @@ The owning zone for a given FQDN is resolved by listing the server's forward-loo * Each validator type manages only its own record type: `MicrosoftAdDomainValidator` reads/writes `TXT`, `MicrosoftAdCnameDomainValidator` reads/writes `CNAME`. Neither touches other record types. * The DNS server must have the `DnsServer` PowerShell module available (installed with the DNS Server role). +### Testing + +There are three levels of testing, each isolating a different layer of the stack. See [test/README.md](test/README.md) for full details; summarized here: + +1. **Infra smoke test** (`test/smoke-test.ps1`) — pure PowerShell, no plugin code. Confirms WinRM reachability, the `DnsServer` module, and the target zone from the machine that will host the gateway. +2. **Provider harness** (`test/ManualTestHarness`) — drives `MicrosoftAdDnsProvider` directly (no gateway, no CA). Exercises TXT create/delete, additive multi-value TXT, targeted delete, CNAME create/delete, and idempotent cleanup against a real DNS server. +3. **Full gateway + CA integration** — a real enrollment through the gateway, a CA, and this plugin together. This is the only level that proves the domain validator is wired up correctly end-to-end (gateway config → CA → DNS-01 challenge → this plugin → DNS server → CA re-check → issuance). + +Public ACME CAs (Let's Encrypt, Google Trust Services, etc.) **reject internal/non-public zones outright** — the order fails at `CreateOrder` with `rejectedIdentifier` / `"Domain must end in a public suffix"` before DNS validation is ever attempted, because `.local`-style names aren't ICANN-delegated public suffixes. This is a CA-side policy check, not a DNS or plugin problem, and it means a public CA can never be used to test level 3 against an internal AD zone. + +To test level 3 against an internal zone (e.g. `command.local`), point the gateway's CA connector at a **private ACME server** instead — [step-ca](https://smallstep.com/docs/step-ca/) works well and doesn't enforce public-suffix rules. See [test/README.md](test/README.md#step-3b--full-gateway-integration-against-an-internal-zone-with-a-private-acme-ca-step-ca) for a full step-ca setup and DNS-resolution troubleshooting walkthrough, including two gotchas that are easy to lose time to: + +* The gateway's own DNS-propagation pre-check defaults to public resolvers (8.8.8.8, 1.1.1.1, etc.), which can never see an internal zone. Point it at an internal DNS server via the CA connector's `DnsVerificationServer` setting. +* The ACME server itself (step-ca) does its **own independent** DNS lookup when validating the challenge — it must be able to resolve the internal zone through its own OS-level resolver, entirely separately from whether the gateway or this plugin can. A DNS-01 order can appear to stage and submit correctly and still hang at `pending` forever if the ACME server's host can't resolve the internal zone. + ### Runtime Requirements - .NET 10.0 runtime (provided by the gateway server)