-
-
Notifications
You must be signed in to change notification settings - Fork 190
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add PowerDNS plugin and guide (#525)
- Loading branch information
Showing
4 changed files
with
365 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,327 @@ | ||
function Get-CurrentPluginType { 'dns-01' } | ||
|
||
function Add-DnsTxt { | ||
[CmdletBinding()] | ||
param( | ||
[Parameter(Mandatory,Position=0)] | ||
[string]$RecordName, | ||
[Parameter(Mandatory,Position=1)] | ||
[string]$TxtValue, | ||
[Parameter(Mandatory)] | ||
[string]$PowerDNSApiHost, | ||
[Parameter(Mandatory)] | ||
[securestring]$PowerDNSApiKey, | ||
[string]$PowerDNSServerName='localhost', | ||
[int]$PowerDNSPort=8081, | ||
[switch]$PowerDNSUseTLS, | ||
[Parameter(ValueFromRemainingArguments)] | ||
$ExtraParams | ||
) | ||
|
||
# get the plaintext version of the api key | ||
$ApiKey = [pscredential]::new('a',$PowerDNSApiKey).GetNetworkCredential().Password | ||
|
||
# build the API root url | ||
$proto = if ($PowerDNSUseTLS) {'https'} else {'http'} | ||
$port = if ($PowerDNSUseTLS -and $PowerDNSPort -eq 443) {''} else {":$PowerDNSPort"} | ||
$ApiBase = "{0}://{1}{2}/api/v1/servers/{3}" -f $proto,$PowerDNSApiHost,$port,$PowerDNSServerName | ||
|
||
Write-Verbose "Attempting to find hosted zone for $RecordName" | ||
$zoneName = Find-Zone $RecordName $ApiBase $ApiKey | ||
if (-not $zoneName) { | ||
throw "Unable to find PowerDNS zone for $RecordName" | ||
} | ||
$zoneBase = '{0}/zones/{1}' -f $ApiBase,$zoneName | ||
|
||
# check if the record already exists | ||
$queryParams = @{ | ||
Uri = '{0}?rrsets=true&rrset_name={1}.&rrset_type=TXT' -f $zoneBase,$RecordName | ||
Headers = @{'X-API-Key' = $ApiKey} | ||
ContentType = 'application/json' | ||
ErrorAction = 'Stop' | ||
Verbose = $false | ||
} | ||
Write-Debug "GET $($queryParams.Uri)" | ||
$rrset = Invoke-RestMethod @queryParams @script:UseBasic | Select-Object -Expand rrsets | ||
|
||
if (-not $rrset) { | ||
# no matching record at all yet | ||
# so build a new one | ||
$rrsets = @{ | ||
rrsets = @( | ||
@{ | ||
name = "$RecordName." | ||
type = 'TXT' | ||
ttl = 60 | ||
changetype = 'REPLACE' | ||
records = @( | ||
@{ content = "`"$TxtValue`"" } | ||
) | ||
} | ||
) | ||
} | ||
} | ||
elseif ("`"$TxtValue`"" -notin $rrset.records.content) { | ||
# no matching value in the existing record | ||
# so add it to the existing rrset | ||
$rrset.records += [pscustomobject]@{content = "`"$TxtValue`""} | ||
$rrset | Add-Member 'changetype' 'REPLACE' | ||
$rrsets = @{ | ||
rrsets = @($rrset) | ||
} | ||
} | ||
else { | ||
Write-Debug "Record $RecordName with value $TxtValue already exists. Nothing to do." | ||
return | ||
} | ||
|
||
# write the updated rrset | ||
$queryParams = @{ | ||
Uri = $zoneBase | ||
Method = 'PATCH' | ||
Body = ($rrsets | ConvertTo-Json -Dep 10) | ||
Headers = @{'X-API-Key' = $ApiKey} | ||
ContentType = 'application/json' | ||
ErrorAction = 'Stop' | ||
Verbose = $false | ||
} | ||
Write-Verbose "Adding $RecordName with value $TxtValue" | ||
Write-Debug "PATCH $($queryParams.Uri)`n$($queryParams.Body)" | ||
Invoke-RestMethod @queryParams @script:UseBasic | ||
|
||
<# | ||
.SYNOPSIS | ||
Add a DNS TXT record to PowerDNS. | ||
.DESCRIPTION | ||
Add a DNS TXT record to PowerDNS. | ||
.PARAMETER RecordName | ||
The fully qualified name of the TXT record. | ||
.PARAMETER TxtValue | ||
The value of the TXT record. | ||
.PARAMETER PowerDNSApiHost | ||
The hostname or IP address of the Power DNS API | ||
.PARAMETER PowerDNSApiKey | ||
The Power DNS API Key | ||
.PARAMETER PowerDNSServerName | ||
The internal name of the server. Defaults to "localhost" | ||
.PARAMETER PowerDNSPort | ||
The TCP port number the API is listening on. Defaults to 8081 | ||
.PARAMETER PowerDNSUseTLS | ||
When specified, try to use HTTPS to connect to the API. Otherwise, HTTP. | ||
.PARAMETER ExtraParams | ||
This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. | ||
.EXAMPLE | ||
$key = Read-Host 'API Key' -AsSecureString | ||
$pluginArgs = @{PowerDNSApiHost='pdns.example.com'; PowerDNSApiKey=$key} | ||
Add-DnsTxt '_acme-challenge.example.com' 'txt-value' @pluginArgs | ||
Adds a TXT record for the specified site/value. | ||
#> | ||
} | ||
|
||
function Remove-DnsTxt { | ||
[CmdletBinding(DefaultParameterSetName='Secure')] | ||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','')] | ||
param( | ||
[Parameter(Mandatory,Position=0)] | ||
[string]$RecordName, | ||
[Parameter(Mandatory,Position=1)] | ||
[string]$TxtValue, | ||
[Parameter(Mandatory)] | ||
[string]$PowerDNSApiHost, | ||
[Parameter(Mandatory)] | ||
[securestring]$PowerDNSApiKey, | ||
[string]$PowerDNSServerName='localhost', | ||
[int]$PowerDNSPort=8081, | ||
[switch]$PowerDNSUseTLS, | ||
[Parameter(ValueFromRemainingArguments)] | ||
$ExtraParams | ||
) | ||
|
||
# get the plaintext version of the api key | ||
$ApiKey = [pscredential]::new('a',$PowerDNSApiKey).GetNetworkCredential().Password | ||
|
||
# build the API root url | ||
$proto = if ($PowerDNSUseTLS) {'https'} else {'http'} | ||
$port = if ($PowerDNSUseTLS -and $PowerDNSPort -eq 443) {''} else {":$PowerDNSPort"} | ||
$ApiBase = "{0}://{1}{2}/api/v1/servers/{3}" -f $proto,$PowerDNSApiHost,$port,$PowerDNSServerName | ||
|
||
Write-Verbose "Attempting to find hosted zone for $RecordName" | ||
$zoneName = Find-Zone $RecordName $ApiBase $ApiKey | ||
if (-not $zoneName) { | ||
throw "Unable to find PowerDNS zone for $RecordName" | ||
} | ||
$zoneBase = '{0}/zones/{1}' -f $ApiBase,$zoneName | ||
|
||
# check if the record already exists | ||
$queryParams = @{ | ||
Uri = '{0}?rrsets=true&rrset_name={1}.&rrset_type=TXT' -f $zoneBase,$RecordName | ||
Headers = @{'X-API-Key' = $ApiKey} | ||
ContentType = 'application/json' | ||
ErrorAction = 'Stop' | ||
Verbose = $false | ||
} | ||
Write-Debug "GET $($queryParams.Uri)" | ||
$rrset = Invoke-RestMethod @queryParams @script:UseBasic | Select-Object -Expand rrsets | ||
|
||
if (-not $rrset -or "`"$TxtValue`"" -notin $rrset.records.content) { | ||
Write-Debug "Record $RecordName with value $TxtValue does not exist. Nothing to do." | ||
return | ||
} | ||
elseif ($rrset.records.Count -gt 1) { | ||
Write-Debug "records count = $($rrset.records.Count)" | ||
# more than one value exists with ours | ||
# so remove it from the existing rrset | ||
$rrset.records = @($rrset.records | Where-Object { $_.content -ne "`"$TxtValue`"" }) | ||
$rrset | Add-Member 'changetype' 'REPLACE' | ||
$rrsets = @{ | ||
rrsets = @($rrset) | ||
} | ||
} | ||
else { | ||
# our value is the only one left, so delete the whole record | ||
$rrset | Add-Member 'changetype' 'DELETE' | ||
$rrsets = @{ | ||
rrsets = @($rrset) | ||
} | ||
} | ||
|
||
# write the updated rrset | ||
$queryParams = @{ | ||
Uri = $zoneBase | ||
Method = 'PATCH' | ||
Body = ($rrsets | ConvertTo-Json -Dep 10) | ||
Headers = @{'X-API-Key' = $ApiKey} | ||
ContentType = 'application/json' | ||
ErrorAction = 'Stop' | ||
Verbose = $false | ||
} | ||
Write-Verbose "Removing $RecordName with value $TxtValue" | ||
Write-Debug "PATCH $($queryParams.Uri)`n$($queryParams.Body)" | ||
Invoke-RestMethod @queryParams @script:UseBasic | ||
|
||
<# | ||
.SYNOPSIS | ||
Remove a DNS TXT record from PowerDNS. | ||
.DESCRIPTION | ||
Remove a DNS TXT record from PowerDNS. | ||
.PARAMETER RecordName | ||
The fully qualified name of the TXT record. | ||
.PARAMETER TxtValue | ||
The value of the TXT record. | ||
.PARAMETER PowerDNSApiHost | ||
The hostname or IP address of the Power DNS API | ||
.PARAMETER PowerDNSApiKey | ||
The Power DNS API Key | ||
.PARAMETER PowerDNSServerName | ||
The internal name of the server. Defaults to "localhost" | ||
.PARAMETER PowerDNSPort | ||
The TCP port number the API is listening on. Defaults to 8081 | ||
.PARAMETER PowerDNSUseTLS | ||
When specified, try to use HTTPS to connect to the API. Otherwise, HTTP. | ||
.PARAMETER ExtraParams | ||
This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. | ||
.EXAMPLE | ||
$key = Read-Host 'API Key' -AsSecureString | ||
$pluginArgs = @{PowerDNSApiHost='pdns.example.com'; PowerDNSApiKey=$key} | ||
Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' @pluginArgs | ||
Removes a TXT record for the specified site/value. | ||
#> | ||
} | ||
|
||
function Save-DnsTxt { | ||
[CmdletBinding()] | ||
param( | ||
[Parameter(ValueFromRemainingArguments)] | ||
$ExtraParams | ||
) | ||
<# | ||
.SYNOPSIS | ||
Not required. | ||
.DESCRIPTION | ||
This provider does not require calling this function to commit changes to DNS records. | ||
.PARAMETER ExtraParams | ||
This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. | ||
#> | ||
} | ||
|
||
############################ | ||
# Helper Functions | ||
############################ | ||
|
||
# https://doc.powerdns.com/authoritative/http-api/index.html#working-with-the-api | ||
|
||
function Find-Zone { | ||
[CmdletBinding()] | ||
param( | ||
[Parameter(Mandatory,Position=0)] | ||
[string]$RecordName, | ||
[Parameter(Mandatory,Position=1)] | ||
[string]$ApiBase, | ||
[Parameter(Mandatory,Position=2)] | ||
[string]$ApiKey | ||
) | ||
|
||
# setup a module variable to cache the record to zone mapping | ||
# so it's quicker to find later | ||
if (!$script:PowerDNSRecordZones) { $script:PowerDNSRecordZones = @{} } | ||
|
||
# check for the record in the cache | ||
if ($script:PowerDNSRecordZones.ContainsKey($RecordName)) { | ||
return $script:PowerDNSRecordZones.$RecordName | ||
} | ||
|
||
# Find the closest/deepest sub-zone that would hold the record. | ||
$pieces = $RecordName.Split('.') | ||
for ($i=0; $i -lt ($pieces.Count-1); $i++) { | ||
$zoneTest = $pieces[$i..($pieces.Count-1)] -join '.' | ||
Write-Debug "Checking $zoneTest" | ||
|
||
try { | ||
$queryParams = @{ | ||
Uri = "$ApiBase/zones/$zoneTest." # PowerDNS very strict about trailing "." | ||
Headers = @{'X-API-Key' = $ApiKey} | ||
ContentType = 'application/json' | ||
ErrorAction = 'Stop' | ||
Verbose = $false | ||
} | ||
Write-Debug "GET $($queryParams.Uri)" | ||
$response = Invoke-RestMethod @queryParams @script:UseBasic | ||
} catch { | ||
# 404 responses mean the zone wasn't found, so skip to the next check | ||
if (404 -eq $_.Exception.Response.StatusCode) { | ||
continue | ||
} | ||
# re-throw anything else | ||
throw | ||
} | ||
|
||
if ($response) { | ||
$script:PowerDNSRecordZones.$RecordName = $response.name | ||
return $response.name | ||
} | ||
} | ||
|
||
return $null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
title: PowerDNS | ||
|
||
# How To Use the PowerDNS Plugin | ||
|
||
This plugin works against the [PowerDNS](https://www.powerdns.com/powerdns-authoritative-server) Authoritative Server. It is assumed that the server is already running with the DNS zone(s) you will be working against. | ||
|
||
## Setup | ||
|
||
You'll need to [enable the API](https://doc.powerdns.com/authoritative/http-api/index.html#enabling-the-api) if it hasn't been already. You'll also need the value of the `api-key` setting from the config file. | ||
|
||
While PowerDNS doesn't directly support using HTTPS against the API, it's possible to do so by running a reverse proxy in front of it which has the certificate and terminates the TLS connections. Make sure you know the hostname or IP and port number of your server as well as whether you need to use HTTP or HTTPS with the API. | ||
|
||
## Using the Plugin | ||
|
||
The minimum parameters you need to provide to the plugin are the hostname or IP address using `PowerDNSApiHost` and the API Key as a SecureString parameter using `PowerDNSApiKey`. This will use an API Url assuming default values for server name (`localhost`), port (`8081`), and use HTTP rather than HTTPS. | ||
|
||
```powershell | ||
$pArgs = @{ | ||
PowerDNSApiHost = 'pdns.example.com' | ||
PowerDNSApiKey = (Read-Host "API Key" -AsSecureString) | ||
} | ||
New-PACertificate example.com -Plugin PowerDNS -PluginArgs $pArgs | ||
``` | ||
|
||
When using an HTTPS reverse proxy in front of the server, you would add `PowerDNSUseTLS`, `PowerDNSPort`, and possibly `PowerDNSServerName` parameters like this: | ||
|
||
```powershell | ||
$pArgs = @{ | ||
PowerDNSApiHost = 'pdns.example.com' | ||
PowerDNSApiKey = (Read-Host "API Key" -AsSecureString) | ||
PowerDNSUseTLS = $true | ||
PowerDNSPort = 443 | ||
PowerDNSServerName = 'localhost' | ||
} | ||
New-PACertificate example.com -Plugin PowerDNS -PluginArgs $pArgs | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters