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 }} diff --git a/README.md b/README.md index 3becce6..14c1b95 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 @@ -101,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) diff --git a/docsource/content.md b/docsource/content.md index 66a6173..d57f949 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 @@ -55,8 +80,6 @@ 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 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: @@ -65,9 +88,7 @@ There are three levels of testing, each isolating a different layer of the stack 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`) - -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: