Step by step LetsEncrypt WinSimple: WILDCARD Edition

This section contains user-submitted tutorials.
Senior user
Posts: 1441
Joined: 2017-09-12 17:57

Step by step LetsEncrypt WinSimple: WILDCARD Edition

I've been looking for a way to create and renew letsencrypt wildcard certificates programatically. I finally stumbled across a script for the rest API for dynu.com, my dns provider. I found the script here: https://github.com/rmbolger/Posh-ACME/t ... DnsPlugins

I haven't tried Posh-ACME because I know and like Win-Acme. However, I found that posh-acme has many powershell scripts for different dns providers and they all have to follow a certain format. All of the scripts contain only functions, which is perfect for easy modification. These scripts are awesome because they already work for many dns provider APIs. This is also a great way to get certificates without having to need a web server for authentication. You only need to add a couple of minor things to make it work with Win-Acme, which sends the following variables to scripts:

{Task} tells your script to CREATE or DELETE the validation dns text entry
{Identifier} is the domain name you're creating the certificate for
{RecordName} is the text record you're creating (_acme-challenge.example.com)
{Token} is the entry for the recordname, which is a unique string used for validation

Modify the script:

Find your dns provider in the list and edit it to add the following.

At the top of the file:

Code: Select all

param(
[string]$Task, [string]$DomainName,
[string]$RecordName, [string]$TxtValue
)

$DynuClientID = 'blah-blah-blah-blah-f8hdkg63jndh'$DynuSecret = 'supersecretstringofbafflegarble'

I looked at a couple of scripts. At the bottom of each function is a description of the credentials you need for that provider. Dynu only requires two things, but, but for example, ClouDNS requires 5 items for credentialing: CDUserType, CDUsername, CDPassword, CDPasswordInsecure, CDPollPropagation. Get all of that information from your account at ClouDNS and turn them into variables as I did with the dynu credential info.

At the bottom of the file:

Code: Select all

if ($Task -eq 'create'){ Add-DnsTxtDynu$RecordName $TxtValue$DynuClientID $DynuSecret } if ($Task -eq 'delete'){
Remove-DnsTxtDynu $RecordName$TxtValue $DynuClientID$DynuSecret
}

This will run CREATE or DELETE based on the parameter sent from Win-Acme. Pay attention to the function names. For Dynu the two functions are Add-DnsTxtDynu and Remove-DnsTxtDynu. For ClouDNS, its Add-DnsTxtClouDNS and Remove-DnsTxtClouDNS.

Additionally, I had to comment out a few instances of the following in the script: @script:UseBasic

Don't ask me why, but it was causing authentication to fail.

To test the script, open powershell and run the script with the 4 parameters discussed above ({Task} {Identifier} {RecordName} {Token}).

Code: Select all

C:\scripts\lews\wacs\Scripts\Dynu.ps1 create example.com blah.example.com some-message-without-spaces
If successful, you should have text record blah.example.com with entry "some-message-without-spaces". Look on your dns provider's admin console to make sure.

Unzip it somewhere, open a command prompt, CD to the win-acme directory and run wacs.exe. A new window will open. The console output below is using v2.0.10.444 of Win-Acme, which (as of today) is the latest stable version.

Code: Select all

 [INFO] A simple Windows ACMEv2 client (WACS)
[INFO] Software version 2.0.10.444 (RELEASE)
[INFO] IIS not detected
[INFO] Please report issues at https://github.com/PKISharp/win-acme

M: Create new certificate (full options)
L: List scheduled renewals
R: Renew scheduled
S: Renew specific
A: Renew *all*
O: More options...
Q: Quit


Choose M to create new certificate.

Code: Select all

 [INFO] Running in mode: Interactive, Advanced

Please specify how the list of domain names that will be included in the
certificate should be determined. If you choose for one of the "all bindings"
options, the list will automatically be updated for future renewals to reflect
the bindings at that time.

1: Manual input
2: Read a CSR created by another program
<Enter>: Abort

How shall we determine the domain(s) to include in the certificate?:

Choose 1 to manual input domain names.

Code: Select all

 Enter comma-separated list of host names, starting with the common name:

Enter the list separated by commas (NO SPACES!!!!). Example: example.com,*.example.com,poopoo.com,*.poopoo.com

You MUST put the wildcard as an alternative name, meaning if you don't, you will not have a certificate for the domain name - it will only work on subdomains. Also, the certificate will be known by the first entry.

Code: Select all

 [INFO] Target generated using plugin Manual: example.com and 1 alternatives

Suggested FriendlyName is '[Manual] example.com', press enter to accept or type an alternative:


Enter anything you want, such as example.com or just hit enter.

Code: Select all

  The ACME server will need to verify that you are the owner of the domain names
that you are requesting the certificate for. This happens both during initial
setup *and* for every future renewal. There are two main methods of doing so:
answering specific http requests (http-01) or create specific dns records
(dns-01). For wildcard domains the latter is the only option. Various
additional plugins are available from https://github.com/PKISharp/win-acme/.

1: [dns-01] Create verification records manually (auto-renew not possible)
2: [dns-01] Create verification records with acme-dns (https://github.com/joohoi/acme-dns)
3: [dns-01] Create verification records with your own script
<Enter>: Abort

How would you like prove ownership for the domain(s) in the certificate?:

Enter 3 for using your own script.

Code: Select all

 Path to script that creates DNS records:

Enter the full path.

Code: Select all

 1: Using the same script
2: Using a different script
3: Do not delete

How to delete records after validation:

Enter 1 to use the same script.

Code: Select all

 {Identifier}:        Domain that's being validated
{RecordName}:        Full TXT record name
{Token}:             Expected value in the TXT record

Input parameters for create script, or enter for default "create {Identifier} {RecordName} {Token}":

Just hit enter for the default values.

Code: Select all

 Input parameters for delete script, or enter for default "delete {Identifier} {RecordName} {Token}":

Just hit enter again for the default values.

Code: Select all

  After ownership of the domain(s) has been proven, we will create a Certificate
Signing Request (CSR) to obtain the actual certificate. The CSR determines
properties of the certificate like which (type of) key to use. If you are not
sure what to pick here, RSA is the safe default.

1: Elliptic Curve key
2: RSA key

What kind of private key should be used for the certificate?:

I used RSA. You may have a reason to use elliptical.

Code: Select all

  When we have the certificate, you can store in one or more ways to make it
accessible to your applications. The Windows Certificate Store is the default
location for IIS (unless you are managing a cluster of them).

1: IIS Central Certificate Store (.pfx per domain)
2: PEM encoded files (Apache, nginx, etc.)
3: Windows Certificate Store
C: Abort

How would you like to store the certificate?:

Enter 2 for PEM. This will work without fuss with hMailServer.

Code: Select all

 Path to folder where .pem files are stored:

Choose a path and enter it.

Code: Select all

 1: IIS Central Certificate Store (.pfx per domain)
2: Windows Certificate Store
3: No additional storage steps required
C: Abort

Would you like to store it in another way too?:

Enter 3 for no additional, unless you want one of the other options.

Code: Select all

  With the certificate now saved to the store(s) of your choice, you may choose
one or more steps to update your applications, e.g. to configure the new
thumbprint, or to update bindings.

1: Start external script or program
2: Do not run any (extra) installation steps

Which installation step should run first?:

Enter 2 for NO EXTRA installation steps.

Code: Select all

 [INFO] Authorize identifier: example.com
[INFO] Authorizing example.com using dns-01 validation (DnsScript)
[INFO] Script C:\scripts\lews\wacs\Scripts\Dynu.ps1 starting with parameters create example.com _acme-challenge.example.com ltWLguTpuTOlWnvCfscQekM5G8J1M74CgX5NYxxBCqU
[INFO] Script finished
[INFO] Answer should now be available at _acme-challenge.example.com
[EROR] Preliminary validation failed

*** I cut out some of the log - I had a "pre-validation" error related to the way win-acme deals with DDNS subdomains.
*** PreValidation is win-acme only and will not affect ACTUAL letsencrypt validation.

[INFO] It looks like validation is going to fail, but we will try now anyway...
[WARN] First chance error calling into ACME server, retrying with new nonce...
[INFO] Authorization result: valid
[INFO] Script C:\scripts\lews\wacs\Scripts\Dynu.ps1 starting with parameters delete example.com _acme-challenge.example.com ltWLguTpuTOlWnvCfscQekM5G8J1M74CgX5NYxxBCqU
[INFO] Script finished
[INFO] Requesting certificate example.com
[INFO] Store with PemFiles...
[INFO] Exporting .pem files to C:\xampp\certificates
[INFO] Installing with None...

Do you want to replace the existing task? (y/n*)  -

If its your first successful run, it will ask to create a scheduled task. Answer yes. After that it doesn't really matter because the task will look to renew all certificates on each run.

Code: Select all

 [INFO] Adding renewal for example.com
[INFO] Next renewal scheduled at 2019/11/14 10:35:58

M: Create new certificate (full options)
L: List scheduled renewals
R: Renew scheduled
S: Renew specific
A: Renew *all*
O: More options...
Q: Quit


Hit Q to quit. That's it. New wildcard certificate with automatic renewal.

Senior user
Posts: 1441
Joined: 2017-09-12 17:57

Re: Step by step LetsEncrypt WinSimple: WILDCARD Edition

Restart apache Powershell script. Run daily via task scheduler. Works with multiple certificates. Looks for the newest one in the certificate store folder and then restarts apache service if within 1 day old. Change $ServiceName to whatever is shown in the Services console > properties to restart the service of your choice - even 'hMailServer'. Code: Select all $ErrorActionPreference = 'silentlycontinue'

$ServiceName = 'Apache2.4'$CommonSvcName = 'Apache'
$CertLocation = 'C:\xampp\certificates' Function SendNotification($Body) {
$EmailFrom = "notifier-account@gmail.com"$EmailTo = "1234567890@tmomail.net"
$SMTPServer = "smtp.gmail.com"$SMTPAuthUser = "notifier-account@gmail.com"
$SMTPAuthPass = "supersecretpassword"$Subject = "Windows Service Notification"
$SMTPClient = New-Object Net.Mail.SmtpClient($SmtpServer, 587)
$SMTPClient.EnableSsl =$true
$SMTPClient.Credentials = New-Object System.Net.NetworkCredential($SMTPAuthUser, $SMTPAuthPass);$SMTPClient.Send($EmailFrom,$EmailTo, $Subject,$Body)
}

$NewestCert = Get-ChildItem -Path$CertLocation | Sort-Object LastWriteTime -Descending | Select-Object -First 1
$LastModifiedDate = (Get-Item$CertLocation\$NewestCert).LastWriteTime If ($LastModifiedDate -gt (Get-Date).AddDays(-1)){
Restart-Service $ServiceName$Body = "$CommonSvcName service RESTARTING to load new SSL certificate." SendNotification$Body
Start-Sleep -seconds 60
(Get-Service $ServiceName).Refresh() If ((Get-Service$ServiceName).Status -ne 'Running'){
Start-Service $ServiceName$Body = "$CommonSvcName service RESTARTING due to a fault (new SSL certificate) - 2nd attempt." SendNotification$Body
Start-Sleep -seconds 60
(Get-Service $ServiceName).Refresh() If ((Get-Service$ServiceName).Status -ne 'Running'){
$Body = "$CommonSvcName service could not be restarted."
SendNotification $Body } } } palinka Senior user Posts: 1441 Joined: 2017-09-12 17:57 Re: Step by step LetsEncrypt WinSimple: WILDCARD Edition From the tutorial: Code: Select all EROR] Preliminary validation failed *** I cut out some of the log - I had a "pre-validation" error related to the way win-acme deals with DDNS subdomains. *** PreValidation is win-acme only and will not affect ACTUAL letsencrypt validation. [INFO] It looks like validation is going to fail, but we will try now anyway... [WARN] First chance error calling into ACME server, retrying with new nonce... [INFO] Authorization result: valid  The developer has confirmed that prevalidation for ddns subdomains is a bug. This does not affect actual validation whatsoever. It just adds a few minutes of wait time while going through prevalidation failing. WouterTinus commented about 1 hour ago This happened because dynu.net is considered to be a TLD by the Public Suffix List and we didn't consider TLD name servers in the prevalidation code, as they are not commonly authoritative for registrerable domains. Though obviously in some cases they are, so they should be considered. This will be fixed in the next release More info here: https://github.com/PKISharp/win-acme/is ... -535660844 radiocooke New user Posts: 3 Joined: 2019-11-30 19:35 Re: Step by step LetsEncrypt WinSimple: WILDCARD Edition Hello, I tried following this guide but am running into a few errors. I'll add that I am new to PowerShell so it's likely something I am overlooking. my dynu.ps1 is at the bottom of my post. The only changes are the beginning and end changes in the instructions from palinka. This is the error I get which appears to be related to the @script:UseBasic lines in the script. Invoke-RestMethod : A positional parameter cannot be found that accepts argument '$null'.
At C:\LetsEncrypt\Scripts\Dynu.ps1:232 char:21
+ ... $response = Invoke-RestMethod -Uri "https://api.dynu.com/v2/oauth2/to ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidArgument: (:) [Invoke-RestMethod], ParentContainsErrorRecordException + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.InvokeRestMethodCommand I found in another forum that adding$script:UseBasic = @{UseBasicParsing=$true} to the top of the script, or (as palinka instructed) removing those lines is a possible fix to the above error. Whether I add$script:UseBasic = @{UseBasicParsing=$true} or remove the$script:UseBasic entries, I get 2 different errors and that is where I'm stumped. here are those errors:
Get-DateTimeOffsetNow : The term 'Get-DateTimeOffsetNow' is not recognized as the name of a cmdlet, function, script file, or operable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At C:\LetsEncrypt\Scripts\Dynu.ps1:245 char:19
+ Expiry = (Get-DateTimeOffsetNow).AddSeconds($response.expires ... + ~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : ObjectNotFound: (Get-DateTimeOffsetNow:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException Invoke-RestMethod : {"statusCode":401,"type":"Authentication Exception","message":"Failed."} At C:\LetsEncrypt\Scripts\Dynu.ps1:40 char:21 + ...$response = Invoke-RestMethod "$apiBase/dns/record/$($RecordName)?rec ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (Method: GET, Reques\u2026PowerShell/6.2.3 }:HttpRequestMessage) [Invoke-RestMethod], HttpResponseException + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand Code: Select all param( [string]$Task,
[string]$DomainName, [string]$RecordName,
[string]$TxtValue )$DynuClientID = '******************************'
$DynuSecret = '********************************' function Add-DnsTxtDynu { [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [string]$RecordName,
[Parameter(Mandatory,Position=1)]
[string]$TxtValue, [Parameter(Mandatory,Position=2)] [string]$DynuClientID,
[Parameter(Mandatory,Position=3)]
[string]$DynuSecret, [Parameter(ValueFromRemainingArguments)]$ExtraParams
)

$apiBase = 'https://api.dynu.com/v2'$RecordName = $RecordName.ToLower() # authenticate$token = Get-DynuAccessToken $DynuClientID$DynuSecret
$headers = @{ Accept = 'application/json' Authorization = "Bearer$token"
}

# The v2 API has a super convenient method for querying a record entirely based on hostname
# so we don't have to deal with the typical find zone id rigamarole.
try {
$response = Invoke-RestMethod "$apiBase/dns/record/$($RecordName)?recordType=TXT" 
-Headers $headers @script:UseBasic } catch { throw } if ($response.dnsRecords -and $TxtValue -in$response.dnsRecords.textData) {
# nothing to do
Write-Debug "Record $RecordName already contains$TxtValue. Nothing to do."
} else {
# if there's at least one record in the response, we can get the zone ID from it, otherwise
# we need to query for the zone ID in before we can add the new record
if ($response.dnsRecords.Count -gt 0) {$zoneID = $response.dnsRecords[0].domainId$recNode = $response.dnsRecords[0].nodeName } else { # query the zone info try {$zoneResp = Invoke-RestMethod "$apiBase/dns/getroot/$RecordName"
-Headers $headers @script:UseBasic } catch { throw } if ($zoneResp -and $zoneResp.id) {$zoneID = $zoneResp.id$recNode = $zoneResp.node } else { throw "No zone info returned for$RecordName from Dynu"
}
}

# now that we have the zone ID, we can add the new record
$bodyJson = @{ nodeName =$recNode
recordType = 'TXT'
textData = $TxtValue state =$true
} | ConvertTo-Json -Compress
try {
Write-Verbose "Adding a TXT record for $RecordName with value$TxtValue"
Invoke-RestMethod "$apiBase/dns/$zoneID/record" -Method Post -Body $bodyJson  -Headers$headers -ContentType 'application/json' @script:UseBasic | Out-Null
} catch { throw }
}

<#
.SYNOPSIS
Add a DNS TXT record to Dynu

.DESCRIPTION
Adds the TXT record to the Dynu zone

.PARAMETER RecordName
The fully qualified name of the TXT record.

.PARAMETER TxtValue
The value of the TXT record.

.PARAMETER DynuClientID
The API Client ID for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials

.PARAMETER DynuSecret
The API Secret for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials

.PARAMETER ExtraParams
This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.

.EXAMPLE

Adds a TXT record for the specified domain with the specified value.
#>
}

function Remove-DnsTxtDynu {
[CmdletBinding()]
param(
[Parameter(Mandatory,Position=0)]
[string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue,
[Parameter(Mandatory,Position=2)]
[string]$DynuClientID, [Parameter(Mandatory,Position=3)] [string]$DynuSecret,
[Parameter(ValueFromRemainingArguments)]
$ExtraParams )$apiBase = 'https://api.dynu.com/v2'
$RecordName =$RecordName.ToLower()

# authenticate
$token = Get-DynuAccessToken$DynuClientID $DynuSecret$headers = @{
Accept = 'application/json'
Authorization = "Bearer $token" } # The v2 API has a super convenient method for querying a record entirely based on hostname # so we don't have to deal with the typical find zone id rigamarole. try {$response = Invoke-RestMethod "$apiBase/dns/record/$($RecordName)?recordType=TXT"  -Headers$headers @script:UseBasic
} catch { throw }

if ($response.dnsRecords -and$TxtValue -in $response.dnsRecords.textData) { # grab the record and delete it$rec = $response.dnsRecords | Where-Object {$_.textData -eq $TxtValue } try { Write-Verbose "Deleting$RecordName with value $TxtValue" Invoke-RestMethod "$apiBase/dns/$($rec.domainId)/record/$($rec.id)" -Method Delete 
-Headers $headers @script:UseBasic | Out-Null } catch { throw } } else { # nothing to do Write-Debug "Record$RecordName with value $TxtValue doesn't exist. Nothing to do." } <# .SYNOPSIS Remove a DNS TXT record from Dynu .DESCRIPTION Removes the TXT record from the Dynu zone .PARAMETER RecordName The fully qualified name of the TXT record. .PARAMETER TxtValue The value of the TXT record. .PARAMETER DynuClientID The API Client ID for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials .PARAMETER DynuSecret The API Secret for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials .PARAMETER ExtraParams This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. .EXAMPLE Remove-DnsTxtDynu '_acme-challenge.site1.example.com' 'asdfqwer12345678' 'dynu_api_client_id' 'dynu_api_client_secret' Removes a TXT record for the specified domain with the specified value. #> } function Save-DnsTxtDynu { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments)]$ExtraParams
)
<#
.SYNOPSIS
Not required

.DESCRIPTION
This provider does not require calling this function to save 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
############################

# API Docs
# https://www.dynu.com/en-US/Resources/API

function Get-DynuAccessToken {
[CmdletBinding()]
param(
[Parameter(Mandatory, Position = 0)]
[string]$DynuClientID, [Parameter(Mandatory, Position = 1)] [string]$DynuSecret
)

if ($script:DynuAccessToken -and ($script:DynuAccessToken.Expiry -lt (Get-DateTimeOffsetNow))) {
return $script:DynuAccessToken.AccessToken } # Dynu's web server requires the credentials to be passed on the first call, so we can't just # use the -Credential parameter because it only adds credentials after passing nothing and getting # an auth challenge response.$encodedCreds = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${DynuClientID}:${DynuSecret}"))
$headers = @{ Accept = 'application/json' Authorization = "Basic$encodedCreds"
}

try {
$response = Invoke-RestMethod -Uri "https://api.dynu.com/v2/oauth2/token" -Headers$headers @script:UseBasic
} catch {
throw
}

if (-not $response.access_token) { Write-Debug ($response | ConvertTo-Json)
}

$script:DynuAccessToken = @{ AccessToken =$response.access_token
Expiry = (Get-DateTimeOffsetNow).AddSeconds($response.expires_in - 300) } return$script:DynuAccessToken.AccessToken
}

if ($Task -eq 'create'){ Add-DnsTxtDynu$RecordName $TxtValue$DynuClientID $DynuSecret } if ($Task -eq 'delete'){
Remove-DnsTxtDynu $RecordName$TxtValue $DynuClientID$DynuSecret
}

Senior user
Posts: 1441
Joined: 2017-09-12 17:57

Re: Step by step LetsEncrypt WinSimple: WILDCARD Edition

2019-11-30 20:36
Hello, I tried following this guide but am running into a few errors. I'll add that I am new to PowerShell so it's likely something I am overlooking.
my dynu.ps1 is at the bottom of my post. The only changes are the beginning and end changes in the instructions from palinka.

This is the error I get which appears to be related to the @script:UseBasic lines in the script.
Invoke-RestMethod : A positional parameter cannot be found that accepts argument '$null'. What happens when you try to run from powershell console? Code: Select all C:\path\to\wacs\Scripts\Dynu.ps1 create example.com DnsTextEntryPrefix.example.com some-test-message-without-spaces Maybe a dumb question, but is dynu.com your domain provider? radiocooke New user Posts: 3 Joined: 2019-11-30 19:35 Re: Step by step LetsEncrypt WinSimple: WILDCARD Edition palinka wrote: 2019-12-01 02:20 radiocooke wrote: 2019-11-30 20:36 Hello, I tried following this guide but am running into a few errors. I'll add that I am new to PowerShell so it's likely something I am overlooking. my dynu.ps1 is at the bottom of my post. The only changes are the beginning and end changes in the instructions from palinka. This is the error I get which appears to be related to the @script:UseBasic lines in the script. Invoke-RestMethod : A positional parameter cannot be found that accepts argument '$null'.
What happens when you try to run from powershell console?

Code: Select all

C:\path\to\wacs\Scripts\Dynu.ps1 create example.com DnsTextEntryPrefix.example.com some-test-message-without-spaces
Maybe a dumb question, but is dynu.com your domain provider?

Dynu is my domain provider, its a paid account, and what you typed is exactly the format of the command I am using in an administrative instance of PowerShell, obviously using my path to Dynu.ps1 and my domain record info.

Correction: I am running the PS command directly form the folder where the Dynu.ps1 file is and the command format I am using is:
.\Dynu.ps1 create example.com DnsTextEntryPrefix.example.com some-test-message-without-spaces
There was a space in the original path to the Dynu.ps1 file I was working with, which is why I was starting the command directly form the file location. As a test I moved the Dynu.ps1 file to a path with no spaces and ran it exactly as your example shows. I got the same errors. I assumed the path was not the issue, but I wanted to confirm.

Senior user
Posts: 1441
Joined: 2017-09-12 17:57

Re: Step by step LetsEncrypt WinSimple: WILDCARD Edition

2019-12-01 03:41
2019-12-01 02:20
2019-11-30 20:36
Hello, I tried following this guide but am running into a few errors. I'll add that I am new to PowerShell so it's likely something I am overlooking.
my dynu.ps1 is at the bottom of my post. The only changes are the beginning and end changes in the instructions from palinka.

This is the error I get which appears to be related to the @script:UseBasic lines in the script.

What happens when you try to run from powershell console?

Code: Select all

C:\path\to\wacs\Scripts\Dynu.ps1 create example.com DnsTextEntryPrefix.example.com some-test-message-without-spaces
Maybe a dumb question, but is dynu.com your domain provider?

Dynu is my domain provider, its a paid account, and what you typed is exactly the format of the command I am using in an administrative instance of PowerShell, obviously using my path to Dynu.ps1 and my domain record info.

Correction: I am running the PS command directly form the folder where the Dynu.ps1 file is and the command format I am using is:
.\Dynu.ps1 create example.com DnsTextEntryPrefix.example.com some-test-message-without-spaces
There was a space in the original path to the Dynu.ps1 file I was working with, which is why I was starting the command directly form the file location. As a test I moved the Dynu.ps1 file to a path with no spaces and ran it exactly as your example shows. I got the same errors. I assumed the path was not the issue, but I wanted to confirm.
Here's my whole script:

Code: Select all

param(
[string]$Task, [string]$DomainName,
[string]$RecordName, [string]$TxtValue
)

$DynuClientID = '_________________________'$DynuSecret = '_________________________'

[CmdletBinding()]
param(
[Parameter(Mandatory,Position=0)]
[string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue,
[Parameter(Mandatory,Position=2)]
[string]$DynuClientID, [Parameter(Mandatory,Position=3)] [string]$DynuSecret,
[Parameter(ValueFromRemainingArguments)]
$ExtraParams )$apiBase = 'https://api.dynu.com/v2'
$RecordName =$RecordName.ToLower()

# authenticate
$token = Get-DynuAccessToken$DynuClientID $DynuSecret$headers = @{
Accept = 'application/json'
Authorization = "Bearer $token" } # The v2 API has a super convenient method for querying a record entirely based on hostname # so we don't have to deal with the typical find zone id rigamarole. try {$response = Invoke-RestMethod "$apiBase/dns/record/$($RecordName)?recordType=TXT"  -Headers$headers #@script:UseBasic
} catch { throw }

if ($response.dnsRecords -and$TxtValue -in $response.dnsRecords.textData) { # nothing to do Write-Debug "Record$RecordName already contains $TxtValue. Nothing to do." } else { # if there's at least one record in the response, we can get the zone ID from it, otherwise # we need to query for the zone ID in before we can add the new record if ($response.dnsRecords.Count -gt 0) {
$zoneID =$response.dnsRecords[0].domainId
$recNode =$response.dnsRecords[0].nodeName
} else {
# query the zone info
try {
$zoneResp = Invoke-RestMethod "$apiBase/dns/getroot/$RecordName"  -Headers$headers #@script:UseBasic
} catch { throw }

if ($zoneResp -and$zoneResp.id) {
$zoneID =$zoneResp.id
$recNode =$zoneResp.node
} else {
throw "No zone info returned for $RecordName from Dynu" } } # now that we have the zone ID, we can add the new record$bodyJson = @{
nodeName = $recNode recordType = 'TXT' textData =$TxtValue
state = $true } | ConvertTo-Json -Compress try { Write-Verbose "Adding a TXT record for$RecordName with value $TxtValue" Invoke-RestMethod "$apiBase/dns/$zoneID/record" -Method Post -Body$bodyJson
-Headers $headers -ContentType 'application/json' | Out-Null #@script:UseBasic | Out-Null } catch { throw } } <# .SYNOPSIS Add a DNS TXT record to Dynu .DESCRIPTION Adds the TXT record to the Dynu zone .PARAMETER RecordName The fully qualified name of the TXT record. .PARAMETER TxtValue The value of the TXT record. .PARAMETER DynuClientID The API Client ID for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials .PARAMETER DynuSecret The API Secret for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials .PARAMETER ExtraParams This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. .EXAMPLE Add-DnsTxtDynu '_acme-challenge.site1.example.com' 'asdfqwer12345678' 'dynu_api_client_id' 'dynu_api_client_secret' Adds a TXT record for the specified domain with the specified value. #> } function Remove-DnsTxtDynu { [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [string]$RecordName,
[Parameter(Mandatory,Position=1)]
[string]$TxtValue, [Parameter(Mandatory,Position=2)] [string]$DynuClientID,
[Parameter(Mandatory,Position=3)]
[string]$DynuSecret, [Parameter(ValueFromRemainingArguments)]$ExtraParams
)

$apiBase = 'https://api.dynu.com/v2'$RecordName = $RecordName.ToLower() # authenticate$token = Get-DynuAccessToken $DynuClientID$DynuSecret
$headers = @{ Accept = 'application/json' Authorization = "Bearer$token"
}

# The v2 API has a super convenient method for querying a record entirely based on hostname
# so we don't have to deal with the typical find zone id rigamarole.
try {
$response = Invoke-RestMethod "$apiBase/dns/record/$($RecordName)?recordType=TXT" 
-Headers $headers #@script:UseBasic } catch { throw } if ($response.dnsRecords -and $TxtValue -in$response.dnsRecords.textData) {
# grab the record and delete it
$rec =$response.dnsRecords | Where-Object { $_.textData -eq$TxtValue }
try {
Write-Verbose "Deleting $RecordName with value$TxtValue"
Invoke-RestMethod "$apiBase/dns/$($rec.domainId)/record/$($rec.id)" -Method Delete  -Headers$headers | Out-Null #@script:UseBasic | Out-Null
} catch { throw }
} else {
# nothing to do
Write-Debug "Record $RecordName with value$TxtValue doesn't exist. Nothing to do."
}

<#
.SYNOPSIS
Remove a DNS TXT record from Dynu

.DESCRIPTION
Removes the TXT record from the Dynu zone

.PARAMETER RecordName
The fully qualified name of the TXT record.

.PARAMETER TxtValue
The value of the TXT record.

.PARAMETER DynuClientID
The API Client ID for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials

.PARAMETER DynuSecret
The API Secret for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials

.PARAMETER ExtraParams
This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.

.EXAMPLE
Remove-DnsTxtDynu '_acme-challenge.site1.example.com' 'asdfqwer12345678' 'dynu_api_client_id' 'dynu_api_client_secret'

Removes a TXT record for the specified domain with the specified value.
#>
}

function Save-DnsTxtDynu {
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments)]
$ExtraParams ) <# .SYNOPSIS Not required .DESCRIPTION This provider does not require calling this function to save 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 ############################ # This function only exists so that we can mock it in Pester tests function Get-DateTimeOffsetNow { [CmdletBinding()] param() [System.DateTimeOffset]::Now } # API Docs # https://www.dynu.com/en-US/Resources/API function Get-DynuAccessToken { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$DynuClientID,
[Parameter(Mandatory, Position = 1)]
[string]$DynuSecret ) if ($script:DynuAccessToken -and ($script:DynuAccessToken.Expiry -lt (Get-DateTimeOffsetNow))) { return$script:DynuAccessToken.AccessToken
}

# Dynu's web server requires the credentials to be passed on the first call, so we can't just
# use the -Credential parameter because it only adds credentials after passing nothing and getting
# an auth challenge response.
$encodedCreds = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${DynuClientID}:${DynuSecret}"))$headers = @{
Accept = 'application/json'
"Authorization" = "Basic ${encodedCreds}" } try {$response = Invoke-RestMethod -Uri "https://api.dynu.com/v2/oauth2/token" -Headers $headers #@script:UseBasic } catch { throw } if (-not$response.access_token) {
Write-Debug ($response | ConvertTo-Json) throw "Access token not found in OAuth2 response from Dynu" }$script:DynuAccessToken = @{
AccessToken = $response.access_token Expiry = (Get-DateTimeOffsetNow).AddSeconds($response.expires_in - 300)
}

return $script:DynuAccessToken.AccessToken } if ($Task -eq 'create'){
Add-DnsTxtDynu $RecordName$TxtValue $DynuClientID$DynuSecret
}

if ($Task -eq 'delete'){ Remove-DnsTxtDynu$RecordName $TxtValue$DynuClientID $DynuSecret }  radiocooke New user Posts: 3 Joined: 2019-11-30 19:35 Re: Step by step LetsEncrypt WinSimple: WILDCARD Edition palinka wrote: 2019-12-01 18:13 radiocooke wrote: 2019-12-01 03:41 palinka wrote: 2019-12-01 02:20 What happens when you try to run from powershell console? Code: Select all C:\path\to\wacs\Scripts\Dynu.ps1 create example.com DnsTextEntryPrefix.example.com some-test-message-without-spaces Maybe a dumb question, but is dynu.com your domain provider? Dynu is my domain provider, its a paid account, and what you typed is exactly the format of the command I am using in an administrative instance of PowerShell, obviously using my path to Dynu.ps1 and my domain record info. Correction: I am running the PS command directly form the folder where the Dynu.ps1 file is and the command format I am using is: .\Dynu.ps1 create example.com DnsTextEntryPrefix.example.com some-test-message-without-spaces There was a space in the original path to the Dynu.ps1 file I was working with, which is why I was starting the command directly form the file location. As a test I moved the Dynu.ps1 file to a path with no spaces and ran it exactly as your example shows. I got the same errors. I assumed the path was not the issue, but I wanted to confirm. Here's my whole script: Code: Select all param( [string]$Task,
[string]$DomainName, [string]$RecordName,
[string]$TxtValue )$DynuClientID = '_________________________'
$DynuSecret = '_________________________' function Add-DnsTxtDynu { [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [string]$RecordName,
[Parameter(Mandatory,Position=1)]
[string]$TxtValue, [Parameter(Mandatory,Position=2)] [string]$DynuClientID,
[Parameter(Mandatory,Position=3)]
[string]$DynuSecret, [Parameter(ValueFromRemainingArguments)]$ExtraParams
)

$apiBase = 'https://api.dynu.com/v2'$RecordName = $RecordName.ToLower() # authenticate$token = Get-DynuAccessToken $DynuClientID$DynuSecret
$headers = @{ Accept = 'application/json' Authorization = "Bearer$token"
}

# The v2 API has a super convenient method for querying a record entirely based on hostname
# so we don't have to deal with the typical find zone id rigamarole.
try {
$response = Invoke-RestMethod "$apiBase/dns/record/$($RecordName)?recordType=TXT" 
-Headers $headers #@script:UseBasic } catch { throw } if ($response.dnsRecords -and $TxtValue -in$response.dnsRecords.textData) {
# nothing to do
Write-Debug "Record $RecordName already contains$TxtValue. Nothing to do."
} else {
# if there's at least one record in the response, we can get the zone ID from it, otherwise
# we need to query for the zone ID in before we can add the new record
if ($response.dnsRecords.Count -gt 0) {$zoneID = $response.dnsRecords[0].domainId$recNode = $response.dnsRecords[0].nodeName } else { # query the zone info try {$zoneResp = Invoke-RestMethod "$apiBase/dns/getroot/$RecordName"
-Headers $headers #@script:UseBasic } catch { throw } if ($zoneResp -and $zoneResp.id) {$zoneID = $zoneResp.id$recNode = $zoneResp.node } else { throw "No zone info returned for$RecordName from Dynu"
}
}

# now that we have the zone ID, we can add the new record
$bodyJson = @{ nodeName =$recNode
recordType = 'TXT'
textData = $TxtValue state =$true
} | ConvertTo-Json -Compress
try {
Write-Verbose "Adding a TXT record for $RecordName with value$TxtValue"
Invoke-RestMethod "$apiBase/dns/$zoneID/record" -Method Post -Body $bodyJson  -Headers$headers -ContentType 'application/json' | Out-Null #@script:UseBasic | Out-Null
} catch { throw }
}

<#
.SYNOPSIS
Add a DNS TXT record to Dynu

.DESCRIPTION
Adds the TXT record to the Dynu zone

.PARAMETER RecordName
The fully qualified name of the TXT record.

.PARAMETER TxtValue
The value of the TXT record.

.PARAMETER DynuClientID
The API Client ID for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials

.PARAMETER DynuSecret
The API Secret for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials

.PARAMETER ExtraParams
This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.

.EXAMPLE

Adds a TXT record for the specified domain with the specified value.
#>
}

function Remove-DnsTxtDynu {
[CmdletBinding()]
param(
[Parameter(Mandatory,Position=0)]
[string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue,
[Parameter(Mandatory,Position=2)]
[string]$DynuClientID, [Parameter(Mandatory,Position=3)] [string]$DynuSecret,
[Parameter(ValueFromRemainingArguments)]
$ExtraParams )$apiBase = 'https://api.dynu.com/v2'
$RecordName =$RecordName.ToLower()

# authenticate
$token = Get-DynuAccessToken$DynuClientID $DynuSecret$headers = @{
Accept = 'application/json'
Authorization = "Bearer $token" } # The v2 API has a super convenient method for querying a record entirely based on hostname # so we don't have to deal with the typical find zone id rigamarole. try {$response = Invoke-RestMethod "$apiBase/dns/record/$($RecordName)?recordType=TXT"  -Headers$headers #@script:UseBasic
} catch { throw }

if ($response.dnsRecords -and$TxtValue -in $response.dnsRecords.textData) { # grab the record and delete it$rec = $response.dnsRecords | Where-Object {$_.textData -eq $TxtValue } try { Write-Verbose "Deleting$RecordName with value $TxtValue" Invoke-RestMethod "$apiBase/dns/$($rec.domainId)/record/$($rec.id)" -Method Delete 
-Headers $headers | Out-Null #@script:UseBasic | Out-Null } catch { throw } } else { # nothing to do Write-Debug "Record$RecordName with value $TxtValue doesn't exist. Nothing to do." } <# .SYNOPSIS Remove a DNS TXT record from Dynu .DESCRIPTION Removes the TXT record from the Dynu zone .PARAMETER RecordName The fully qualified name of the TXT record. .PARAMETER TxtValue The value of the TXT record. .PARAMETER DynuClientID The API Client ID for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials .PARAMETER DynuSecret The API Secret for the Dynu account. Can be found at https://www.dynu.com/en-US/ControlPanel/APICredentials .PARAMETER ExtraParams This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. .EXAMPLE Remove-DnsTxtDynu '_acme-challenge.site1.example.com' 'asdfqwer12345678' 'dynu_api_client_id' 'dynu_api_client_secret' Removes a TXT record for the specified domain with the specified value. #> } function Save-DnsTxtDynu { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments)]$ExtraParams
)
<#
.SYNOPSIS
Not required

.DESCRIPTION
This provider does not require calling this function to save 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
############################

# This function only exists so that we can mock it in Pester tests
function Get-DateTimeOffsetNow {
[CmdletBinding()]
param()
[System.DateTimeOffset]::Now
}

# API Docs
# https://www.dynu.com/en-US/Resources/API

function Get-DynuAccessToken {
[CmdletBinding()]
param(
[Parameter(Mandatory, Position = 0)]
[string]$DynuClientID, [Parameter(Mandatory, Position = 1)] [string]$DynuSecret
)

if ($script:DynuAccessToken -and ($script:DynuAccessToken.Expiry -lt (Get-DateTimeOffsetNow))) {
return $script:DynuAccessToken.AccessToken } # Dynu's web server requires the credentials to be passed on the first call, so we can't just # use the -Credential parameter because it only adds credentials after passing nothing and getting # an auth challenge response.$encodedCreds = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${DynuClientID}:${DynuSecret}"))
$headers = @{ Accept = 'application/json' "Authorization" = "Basic${encodedCreds}"
}

try {
$response = Invoke-RestMethod -Uri "https://api.dynu.com/v2/oauth2/token" -Headers$headers #@script:UseBasic
} catch {
throw
}

if (-not $response.access_token) { Write-Debug ($response | ConvertTo-Json)
}

$script:DynuAccessToken = @{ AccessToken =$response.access_token
Expiry = (Get-DateTimeOffsetNow).AddSeconds($response.expires_in - 300) } return$script:DynuAccessToken.AccessToken
}

if ($Task -eq 'create'){ Add-DnsTxtDynu$RecordName $TxtValue$DynuClientID $DynuSecret } if ($Task -eq 'delete'){
Remove-DnsTxtDynu $RecordName$TxtValue $DynuClientID$DynuSecret
}



Thank you very much. That helped a lot. My script was missing a section that defined the Get-DateTimeOffsetNow function:

Code: Select all

function Get-DateTimeOffsetNow {
[CmdletBinding()]
param()
[System.DateTimeOffset]::Now
}`

This appeared to be the cause of the Get-DateTimeOffsetNow error. I went back and looked at the source file
and that line of code is not in it, so it does not appear to be something I deleted by mistake. After that I just needed to create new API keys which got rid of the 401 error and now it's working, thanks again for your help!