From f21473f28860d1952f24c4bb016f27e5d19374a6 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Thu, 1 Aug 2024 12:07:19 +0100 Subject: [PATCH] Add more staging cert authorities --- step-templates/letsencrypt-azure-dns.json | 11 +++++------ step-templates/letsencrypt-cloudflare.json | 11 +++++------ step-templates/letsencrypt-dnsimple.json | 13 ++++++------- step-templates/letsencrypt-google-cloud.json | 11 +++++------ step-templates/letsencrypt-route-53.json | 11 +++++------ step-templates/letsencrypt-selfhosted-http.json | 11 +++++------ 6 files changed, 31 insertions(+), 37 deletions(-) diff --git a/step-templates/letsencrypt-azure-dns.json b/step-templates/letsencrypt-azure-dns.json index 64fd6ad73..c062bcbaa 100644 --- a/step-templates/letsencrypt-azure-dns.json +++ b/step-templates/letsencrypt-azure-dns.json @@ -3,12 +3,12 @@ "Name": "Lets Encrypt - Azure DNS", "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). \n\n#### Features\n\n- ACME v2 protocol support which allows generating wildcard certificates (*.example.com)\n- [Azure DNS](https://azure.microsoft.com/en-us/services/dns/) Challenge for TLD, CNAME and Wildcard domains. \n- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). \n- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", "ActionType": "Octopus.Script", - "Version": 11, + "Version": 12, "Packages": [], "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_AzureDNS_CertificateDomain = $OctopusParameters[\"LE_AzureDNS_CertificateDomain\"]\n$LE_AzureDNS_CertificateName = \"Lets Encrypt - $($LE_AzureDNS_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_AzureDNS_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\")\n$LE_AzureDNS_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_AzureDNS_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n $azure_password = ConvertTo-SecureString -String $OctopusParameters[\"LE_AzureDNS_AzureAccount.Password\"] -AsPlainText -Force\n $azure_credential = New-Object System.Management.Automation.PSCredential($OctopusParameters[\"LE_AzureDNS_AzureAccount.Client\"], $azure_password)\n $azure_params = @{\n AZSubscriptionId = $OctopusParameters[\"LE_AzureDNS_AzureAccount.SubscriptionNumber\"];\n AZTenantId = $OctopusParameters[\"LE_AzureDNS_AzureAccount.TenantId\"];\n AZAppCred = $azure_credential\n }\n\n try {\n\n $DnsPlugins = @(\"Azure\")\n $DomainList = @($LE_AzureDNS_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_AzureDNS_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_AzureDNS_CreateWildcardSAN\"] -eq $True) {\n $LE_AzureDNS_Certificate_SAN = $LE_AzureDNS_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_AzureDNS_Certificate_SAN\n # Include additional DnsPlugin of same type to suppress warning.\n $DnsPlugins += \"Azure\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_AzureDNS_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $azure_params;\n PfxPass = $OctopusParameters[\"LE_AzureDNS_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_AzureDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_AzureDNS_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_AzureDNS_Issuers\n if ($OctopusParameters[\"LE_AzureDNS_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_AzureDNS_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_AzureDNS_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate is required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_AzureDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_AzureDNS_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_AzureDNS_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_AzureDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_AzureDNS_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_AzureDNS_CertificateDomain) certificate. Error: $($_.Exception.Message)\"\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_AzureDNS_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_AzureDNS_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_AzureDNS_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_AzureDNS_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_AzureDNS_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_AzureDNS_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_AzureDNS_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_AzureDNS_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_AzureDNS_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_AzureDNS_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_AzureDNS_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", + "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_AzureDNS_CertificateDomain = $OctopusParameters[\"LE_AzureDNS_CertificateDomain\"]\n$LE_AzureDNS_CertificateName = \"Lets Encrypt - $($LE_AzureDNS_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_AzureDNS_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\", \"(STAGING) Pseudo Plum E5\", \"(STAGING) False Fennel E6\", \"(STAGING) Puzzling Parsnip E7\", \"(STAGING) Mysterious Mulberry E8\", \"(STAGING) Fake Fig E9\", \"(STAGING) Counterfeit Cashew R10\", \"(STAGING) Wannabe Watercress R11\", \"(STAGING) Riddling Rhubarb R12\", \"(STAGING) Tenuous Tomato R13\", \"(STAGING) Not Nectarine R14\")\n$LE_AzureDNS_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_AzureDNS_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n $azure_password = ConvertTo-SecureString -String $OctopusParameters[\"LE_AzureDNS_AzureAccount.Password\"] -AsPlainText -Force\n $azure_credential = New-Object System.Management.Automation.PSCredential($OctopusParameters[\"LE_AzureDNS_AzureAccount.Client\"], $azure_password)\n $azure_params = @{\n AZSubscriptionId = $OctopusParameters[\"LE_AzureDNS_AzureAccount.SubscriptionNumber\"];\n AZTenantId = $OctopusParameters[\"LE_AzureDNS_AzureAccount.TenantId\"];\n AZAppCred = $azure_credential\n }\n\n try {\n\n $DnsPlugins = @(\"Azure\")\n $DomainList = @($LE_AzureDNS_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_AzureDNS_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_AzureDNS_CreateWildcardSAN\"] -eq $True) {\n $LE_AzureDNS_Certificate_SAN = $LE_AzureDNS_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_AzureDNS_Certificate_SAN\n # Include additional DnsPlugin of same type to suppress warning.\n $DnsPlugins += \"Azure\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_AzureDNS_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $azure_params;\n PfxPass = $OctopusParameters[\"LE_AzureDNS_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_AzureDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_AzureDNS_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_AzureDNS_Issuers\n if ($OctopusParameters[\"LE_AzureDNS_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_AzureDNS_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_AzureDNS_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate is required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_AzureDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_AzureDNS_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_AzureDNS_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_AzureDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_AzureDNS_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_AzureDNS_CertificateDomain) certificate. Error: $($_.Exception.Message)\"\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_AzureDNS_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_AzureDNS_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_AzureDNS_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_AzureDNS_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_AzureDNS_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_AzureDNS_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_AzureDNS_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_AzureDNS_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_AzureDNS_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_AzureDNS_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_AzureDNS_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", "Octopus.Action.SubstituteInFiles.Enabled": "True" }, "Parameters": [{ @@ -92,12 +92,11 @@ } } ], - "LastModifiedAt": "2022-02-07T09:38:11.788Z", "$Meta": { - "ExportedAt": "2024-06-24T06:57:36.821Z", - "OctopusVersion": "2024.3.4152", + "ExportedAt": "2024-08-01T10:57:00.608Z", + "OctopusVersion": "2024.3.8336", "Type": "ActionTemplate" }, - "LastModifiedBy": "benjimac93", + "LastModifiedBy": "harrisonmeister", "Category": "lets-encrypt" } diff --git a/step-templates/letsencrypt-cloudflare.json b/step-templates/letsencrypt-cloudflare.json index 834c9f3a6..2719a360e 100644 --- a/step-templates/letsencrypt-cloudflare.json +++ b/step-templates/letsencrypt-cloudflare.json @@ -3,13 +3,13 @@ "Name": "Lets Encrypt - Cloudflare", "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). \n\n#### Features\n\n- ACME v2 protocol support which allows generating wildcard certificates (*.example.com)\n- [Cloudflare DNS](https://www.cloudflare.com/en-au/dns/) Challenge for TLD, CNAME and Wildcard domains. \n- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). \n- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", "ActionType": "Octopus.Script", - "Version": 10, + "Version": 11, "CommunityActionTemplateId": null, "Packages": [], "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_Cloudflare_CertificateDomain = $OctopusParameters[\"LE_Cloudflare_CertificateDomain\"]\n$LE_Cloudflare_CertificateName = \"Lets Encrypt - $($LE_Cloudflare_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_Cloudflare_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\")\n$LE_Cloudflare_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_Cloudflare_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n # Cloudflare API tokens require some special wrangling.\n $cloudflare_token = ConvertTo-SecureString -String $OctopusParameters[\"LE_Cloudflare_PrimaryToken\"] -AsPlainText -Force\n $cloudflare_args = @{\n CFToken = $cloudflare_token\n }\n\n if ($OctopusParameters[\"LE_Cloudflare_SecondaryToken\"]) {\n Write-Debug \"LE_Cloudflare_SecondaryToken has a value. Passing it to the Cloudflare DNS plugin as a Read All Token.\"\n $cloudflare_token_secondary = ConvertTo-SecureString -String $OctopusParameters[\"LE_Cloudflare_SecondaryToken\"] -AsPlainText -Force\n $cloudflare_args.CFTokenReadAll = $cloudflare_token_secondary\n }\n\n try {\n\n $DnsPlugins = @(\"Cloudflare\")\n $DomainList = @($LE_Cloudflare_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_Cloudflare_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_Cloudflare_CreateWildcardSAN\"] -eq $True) {\n $LE_Cloudflare_Certificate_SAN = $LE_Cloudflare_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_Cloudflare_Certificate_SAN\n # Include additional DnsPlugin of same type to suppress warning.\n $DnsPluginList += \"Cloudflare\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_Cloudflare_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $cloudflare_args;\n PfxPass = $OctopusParameters[\"LE_Cloudflare_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Cloudflare_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_Cloudflare_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_Cloudflare_Issuers\n if ($OctopusParameters[\"LE_Cloudflare_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_Cloudflare_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_Cloudflare_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate is required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Cloudflare_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_Cloudflare_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_Cloudflare_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Cloudflare_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_Cloudflare_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_Cloudflare_CertificateDomain) certificate. Error: $($_.Exception.Message)\"\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_Cloudflare_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_Cloudflare_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_Cloudflare_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_Cloudflare_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_Cloudflare_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_Cloudflare_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_Cloudflare_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_Cloudflare_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_Cloudflare_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_Cloudflare_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_Cloudflare_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", + "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_Cloudflare_CertificateDomain = $OctopusParameters[\"LE_Cloudflare_CertificateDomain\"]\n$LE_Cloudflare_CertificateName = \"Lets Encrypt - $($LE_Cloudflare_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_Cloudflare_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\", \"(STAGING) Pseudo Plum E5\", \"(STAGING) False Fennel E6\", \"(STAGING) Puzzling Parsnip E7\", \"(STAGING) Mysterious Mulberry E8\", \"(STAGING) Fake Fig E9\", \"(STAGING) Counterfeit Cashew R10\", \"(STAGING) Wannabe Watercress R11\", \"(STAGING) Riddling Rhubarb R12\", \"(STAGING) Tenuous Tomato R13\", \"(STAGING) Not Nectarine R14\")\n$LE_Cloudflare_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_Cloudflare_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n # Cloudflare API tokens require some special wrangling.\n $cloudflare_token = ConvertTo-SecureString -String $OctopusParameters[\"LE_Cloudflare_PrimaryToken\"] -AsPlainText -Force\n $cloudflare_args = @{\n CFToken = $cloudflare_token\n }\n\n if ($OctopusParameters[\"LE_Cloudflare_SecondaryToken\"]) {\n Write-Debug \"LE_Cloudflare_SecondaryToken has a value. Passing it to the Cloudflare DNS plugin as a Read All Token.\"\n $cloudflare_token_secondary = ConvertTo-SecureString -String $OctopusParameters[\"LE_Cloudflare_SecondaryToken\"] -AsPlainText -Force\n $cloudflare_args.CFTokenReadAll = $cloudflare_token_secondary\n }\n\n try {\n\n $DnsPlugins = @(\"Cloudflare\")\n $DomainList = @($LE_Cloudflare_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_Cloudflare_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_Cloudflare_CreateWildcardSAN\"] -eq $True) {\n $LE_Cloudflare_Certificate_SAN = $LE_Cloudflare_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_Cloudflare_Certificate_SAN\n # Include additional DnsPlugin of same type to suppress warning.\n $DnsPluginList += \"Cloudflare\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_Cloudflare_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $cloudflare_args;\n PfxPass = $OctopusParameters[\"LE_Cloudflare_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Cloudflare_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_Cloudflare_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_Cloudflare_Issuers\n if ($OctopusParameters[\"LE_Cloudflare_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_Cloudflare_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_Cloudflare_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate is required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Cloudflare_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_Cloudflare_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_Cloudflare_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Cloudflare_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_Cloudflare_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_Cloudflare_CertificateDomain) certificate. Error: $($_.Exception.Message)\"\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_Cloudflare_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_Cloudflare_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_Cloudflare_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_Cloudflare_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_Cloudflare_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_Cloudflare_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_Cloudflare_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_Cloudflare_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_Cloudflare_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_Cloudflare_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_Cloudflare_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", "Octopus.Action.SubstituteInFiles.Enabled": "True" }, "Parameters": [{ @@ -103,12 +103,11 @@ } } ], - "LastModifiedAt": "2022-02-07T09:38:11.788Z", "$Meta": { - "ExportedAt": "2024-06-24T06:57:36.821Z", - "OctopusVersion": "2024.3.4152", + "ExportedAt": "2024-08-01T10:57:00.608Z", + "OctopusVersion": "2024.3.8336", "Type": "ActionTemplate" }, - "LastModifiedBy": "benjimac93", + "LastModifiedBy": "harrisonmeister", "Category": "lets-encrypt" } diff --git a/step-templates/letsencrypt-dnsimple.json b/step-templates/letsencrypt-dnsimple.json index 5f79e4757..f35db59dd 100644 --- a/step-templates/letsencrypt-dnsimple.json +++ b/step-templates/letsencrypt-dnsimple.json @@ -3,7 +3,7 @@ "Name": "Lets Encrypt - DNSimple", "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). \n\n#### Features\n\n- ACME v2 protocol support which allows generating wildcard certificates (*.example.com)\n- [DNSimple](https://dnsimple.com/) Challenge for TLD, CNAME and Wildcard domains. \n- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). \n- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", "ActionType": "Octopus.Script", - "Version": 7, + "Version": 8, "CommunityActionTemplateId": null, "Packages": [ @@ -11,7 +11,7 @@ "Properties":{ "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# DebugOutput\n###############################################################################\nif ($OctopusParameters[\"LE_DNSimple_Debug_Output\"] -eq $True) {\n\tWrite-Host \"Setting DebugPreference to Continue\"\n $DebugPreference = 'Continue'\n}\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_DNSimple_CertificateDomain = $OctopusParameters[\"LE_DNSimple_CertificateDomain\"]\n$LE_DNSimple_CertificateName = \"Lets Encrypt - $($LE_DNSimple_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_DNSimple_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\")\n$LE_DNSimple_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_DNSimple_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n\t$dnsimple_args = @{}\n # DNSimple requires a token. If it's windows, Secure-String is supported.\n if ($IsWindows -and 'Desktop' -eq $PSEdition) {\n $token = ConvertTo-SecureString -String $OctopusParameters[\"LE_DNSimple_Token\"] -AsPlainText -Force\n \t$dnsimple_args = @{\n \tDSToken = $token\n \t}\n }\n else {\n \t$token = $OctopusParameters[\"LE_DNSimple_Token\"]\n \t$dnsimple_args = @{\n \tDSTokenInsecure = $token\n \t}\n }\n \n try {\n\n $DnsPlugins = @(\"DNSimple\")\n $DomainList = @($LE_DNSimple_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_DNSimple_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_DNSimple_CreateWildcardSAN\"] -eq $True) {\n $LE_DNSimple_Certificate_SAN = $LE_DNSimple_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_DNSimple_Certificate_SAN\n # Include additional DnsPlugin of same type to surpress warning.\n $DnsPlugins += \"DNSimple\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_DNSimple_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $dnsimple_args;\n PfxPass = $OctopusParameters[\"LE_DNSimple_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_DNSimple_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_DNSimple_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_DNSimple_Issuers\n if ($OctopusParameters[\"LE_DNSimple_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_DNSimple_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_DNSimple_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate is required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_DNSimple_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_DNSimple_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_DNSimple_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_DNSimple_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_DNSimple_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_DNSimple_CertificateDomain) certificate. Error: $($_.Exception.Message)\"\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_DNSimple_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_DNSimple_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_DNSimple_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_DNSimple_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_DNSimple_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_DNSimple_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_DNSimple_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_DNSimple_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_DNSimple_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_DNSimple_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_DNSimple_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", + "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# DebugOutput\n###############################################################################\nif ($OctopusParameters[\"LE_DNSimple_Debug_Output\"] -eq $True) {\n\tWrite-Host \"Setting DebugPreference to Continue\"\n $DebugPreference = 'Continue'\n}\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_DNSimple_CertificateDomain = $OctopusParameters[\"LE_DNSimple_CertificateDomain\"]\n$LE_DNSimple_CertificateName = \"Lets Encrypt - $($LE_DNSimple_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_DNSimple_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\", \"(STAGING) Pseudo Plum E5\", \"(STAGING) False Fennel E6\", \"(STAGING) Puzzling Parsnip E7\", \"(STAGING) Mysterious Mulberry E8\", \"(STAGING) Fake Fig E9\", \"(STAGING) Counterfeit Cashew R10\", \"(STAGING) Wannabe Watercress R11\", \"(STAGING) Riddling Rhubarb R12\", \"(STAGING) Tenuous Tomato R13\", \"(STAGING) Not Nectarine R14\")\n$LE_DNSimple_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_DNSimple_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n\t$dnsimple_args = @{}\n # DNSimple requires a token. If it's windows, Secure-String is supported.\n if ($IsWindows -and 'Desktop' -eq $PSEdition) {\n $token = ConvertTo-SecureString -String $OctopusParameters[\"LE_DNSimple_Token\"] -AsPlainText -Force\n \t$dnsimple_args = @{\n \tDSToken = $token\n \t}\n }\n else {\n \t$token = $OctopusParameters[\"LE_DNSimple_Token\"]\n \t$dnsimple_args = @{\n \tDSTokenInsecure = $token\n \t}\n }\n \n try {\n\n $DnsPlugins = @(\"DNSimple\")\n $DomainList = @($LE_DNSimple_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_DNSimple_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_DNSimple_CreateWildcardSAN\"] -eq $True) {\n $LE_DNSimple_Certificate_SAN = $LE_DNSimple_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_DNSimple_Certificate_SAN\n # Include additional DnsPlugin of same type to surpress warning.\n $DnsPlugins += \"DNSimple\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_DNSimple_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $dnsimple_args;\n PfxPass = $OctopusParameters[\"LE_DNSimple_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_DNSimple_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_DNSimple_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_DNSimple_Issuers\n if ($OctopusParameters[\"LE_DNSimple_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_DNSimple_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_DNSimple_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate is required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_DNSimple_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_DNSimple_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_DNSimple_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_DNSimple_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_DNSimple_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_DNSimple_CertificateDomain) certificate. Error: $($_.Exception.Message)\"\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_DNSimple_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_DNSimple_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_DNSimple_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_DNSimple_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_DNSimple_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_DNSimple_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_DNSimple_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_DNSimple_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_DNSimple_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_DNSimple_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_DNSimple_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", "Octopus.Action.SubstituteInFiles.Enabled": "True" }, "Parameters": [ @@ -105,13 +105,12 @@ "Octopus.ControlType": "Checkbox" } } - ], - "LastModifiedAt": "2022-02-07T09:38:11.788Z", + ], "$Meta": { - "ExportedAt": "2024-06-24T06:57:36.821Z", - "OctopusVersion": "2024.3.4152", + "ExportedAt": "2024-08-01T10:57:00.608Z", + "OctopusVersion": "2024.3.8336", "Type": "ActionTemplate" }, - "LastModifiedBy": "benjimac93", + "LastModifiedBy": "harrisonmeister", "Category": "lets-encrypt" } diff --git a/step-templates/letsencrypt-google-cloud.json b/step-templates/letsencrypt-google-cloud.json index 6e5e406d3..858a34955 100644 --- a/step-templates/letsencrypt-google-cloud.json +++ b/step-templates/letsencrypt-google-cloud.json @@ -3,13 +3,13 @@ "Name": "Lets Encrypt - Google Cloud DNS", "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). \n\n#### Features\n\n- ACME v2 protocol support which allows generating wildcard certificates (*.example.com)\n- [Google Cloud DNS](https://cloud.google.com/dns) Challenge for TLD, CNAME and Wildcard domains. \n- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). \n- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", "ActionType": "Octopus.Script", - "Version": 11, + "Version": 12, "CommunityActionTemplateId": null, "Packages": [], "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_GCloudDNS_CertificateDomain = $OctopusParameters[\"LE_GCloudDNS_CertificateDomain\"]\n$LE_GCloudDNS_CertificateName = \"Lets Encrypt - $($LE_GCloudDNS_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_GCloudDNS_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\")\n$LE_GCloudDNS_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_GCloudDNS_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n # Google Cloud requires JSON data to be passed in.\n $gcloud_json = \"$(New-Guid).json\"\n Set-Content -Path $gcloud_json -Value $OctopusParameters[\"LE_GCloudDNS_JSON\"]\n\n $gcloud_args = @{\n GCKeyFile = $gcloud_json\n }\n\n try {\n $DnsPlugins = @(\"GCloud\")\n $DomainList = @($LE_GCloudDNS_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_GCloudDNS_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_GCloudDNS_CreateWildcardSAN\"] -eq $True) {\n $LE_GCloudDNS_Certificate_SAN = $LE_GCloudDNS_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_GCloudDNS_Certificate_SAN\n # Include additional DnsPlugin of same type to surpress warning.\n $DnsPlugins += \"GCloud\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_GCloudDNS_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $gcloud_args;\n PfxPass = $OctopusParameters[\"LE_GCloudDNS_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_GCloudDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_GCloudDNS_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_GCloudDNS_Issuers\n if ($OctopusParameters[\"LE_GCloudDNS_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_GCloudDNS_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_GCloudDNS_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate is required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_GCloudDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_GCloudDNS_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_GCloudDNS_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_GCloudDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_GCloudDNS_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_GCloudDNS_CertificateDomain) certificate. Error: $($_.Exception.Message)\"\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_GCloudDNS_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_GCloudDNS_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_GCloudDNS_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_GCloudDNS_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_GCloudDNS_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_GCloudDNS_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_GCloudDNS_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_GCloudDNS_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_GCloudDNS_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_GCloudDNS_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_GCloudDNS_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", + "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_GCloudDNS_CertificateDomain = $OctopusParameters[\"LE_GCloudDNS_CertificateDomain\"]\n$LE_GCloudDNS_CertificateName = \"Lets Encrypt - $($LE_GCloudDNS_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_GCloudDNS_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\", \"(STAGING) Pseudo Plum E5\", \"(STAGING) False Fennel E6\", \"(STAGING) Puzzling Parsnip E7\", \"(STAGING) Mysterious Mulberry E8\", \"(STAGING) Fake Fig E9\", \"(STAGING) Counterfeit Cashew R10\", \"(STAGING) Wannabe Watercress R11\", \"(STAGING) Riddling Rhubarb R12\", \"(STAGING) Tenuous Tomato R13\", \"(STAGING) Not Nectarine R14\")\n$LE_GCloudDNS_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_GCloudDNS_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n # Google Cloud requires JSON data to be passed in.\n $gcloud_json = \"$(New-Guid).json\"\n Set-Content -Path $gcloud_json -Value $OctopusParameters[\"LE_GCloudDNS_JSON\"]\n\n $gcloud_args = @{\n GCKeyFile = $gcloud_json\n }\n\n try {\n $DnsPlugins = @(\"GCloud\")\n $DomainList = @($LE_GCloudDNS_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_GCloudDNS_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_GCloudDNS_CreateWildcardSAN\"] -eq $True) {\n $LE_GCloudDNS_Certificate_SAN = $LE_GCloudDNS_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_GCloudDNS_Certificate_SAN\n # Include additional DnsPlugin of same type to surpress warning.\n $DnsPlugins += \"GCloud\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_GCloudDNS_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $gcloud_args;\n PfxPass = $OctopusParameters[\"LE_GCloudDNS_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_GCloudDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_GCloudDNS_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_GCloudDNS_Issuers\n if ($OctopusParameters[\"LE_GCloudDNS_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_GCloudDNS_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_GCloudDNS_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate is required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_GCloudDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_GCloudDNS_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_GCloudDNS_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_GCloudDNS_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_GCloudDNS_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_GCloudDNS_CertificateDomain) certificate. Error: $($_.Exception.Message)\"\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_GCloudDNS_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_GCloudDNS_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_GCloudDNS_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_GCloudDNS_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_GCloudDNS_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_GCloudDNS_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_GCloudDNS_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_GCloudDNS_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_GCloudDNS_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_GCloudDNS_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_GCloudDNS_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", "Octopus.Action.SubstituteInFiles.Enabled": "True" }, "Parameters": [{ @@ -93,12 +93,11 @@ } } ], - "LastModifiedAt": "2022-02-07T09:38:11.788Z", "$Meta": { - "ExportedAt": "2024-06-24T06:57:36.821Z", - "OctopusVersion": "2024.3.4152", + "ExportedAt": "2024-08-01T10:57:00.608Z", + "OctopusVersion": "2024.3.8336", "Type": "ActionTemplate" }, - "LastModifiedBy": "benjimac93", + "LastModifiedBy": "harrisonmeister", "Category": "lets-encrypt" } diff --git a/step-templates/letsencrypt-route-53.json b/step-templates/letsencrypt-route-53.json index c125f06e6..0650d1615 100644 --- a/step-templates/letsencrypt-route-53.json +++ b/step-templates/letsencrypt-route-53.json @@ -3,12 +3,12 @@ "Name": "Lets Encrypt - Route53", "Description": "Request (or renew) a X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/). \n\n#### Features\n\n- ACME v2 protocol support which allows generating wildcard certificates (*.example.com)\n- [AWS Route53](https://aws.amazon.com/route53/) Challenge for TLD, CNAME and Wildcard domains. \n- Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates). \n- Verified to work on both Windows (PowerShell 5+) and Linux (PowerShell 6+) deployment Targets or Workers.", "ActionType": "Octopus.Script", - "Version": 12, + "Version": 13, "Packages": [], "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_Route53_CertificateDomain = $OctopusParameters[\"LE_Route53_CertificateDomain\"]\n$LE_Route53_CertificateName = \"Lets Encrypt - $($LE_Route53_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_Route53_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\")\n$LE_Route53_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_Route53_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n $aws_secret_key = ConvertTo-SecureString -String $OctopusParameters[\"LE_Route53_AWSAccount.SecretKey\"] -AsPlainText -Force\n $route53_params = @{\n R53AccessKey = $OctopusParameters[\"LE_Route53_AWSAccount.AccessKey\"];\n R53SecretKey = $aws_secret_key\n }\n\n try {\n $DnsPlugins = @(\"Route53\")\n $DomainList = @($LE_Route53_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_Route53_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_Route53_CreateWildcardSAN\"] -eq $True) {\n $LE_Route53_Certificate_SAN = $LE_Route53_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_Route53_Certificate_SAN\n # Include additional DnsPlugin of same type to surpress warning.\n $DnsPlugins += \"Route53\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_Route53_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $route53_params;\n PfxPass = $OctopusParameters[\"LE_Route53_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Route53_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_Route53_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_Route53_Issuers\n if ($OctopusParameters[\"LE_Route53_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_Route53_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_Route53_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Route53_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_Route53_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_Route53_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Route53_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_Route53_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_Route53_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_Route53_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_Route53_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_Route53_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_Route53_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_Route53_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_Route53_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_Route53_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_Route53_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_Route53_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_Route53_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_Route53_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", + "Octopus.Action.Script.ScriptBody": "###############################################################################\n# TLS 1.2\n###############################################################################\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nImport-Module Posh-ACME\n\n###############################################################################\n# Constants\n###############################################################################\n$LE_Route53_CertificateDomain = $OctopusParameters[\"LE_Route53_CertificateDomain\"]\n$LE_Route53_CertificateName = \"Lets Encrypt - $($LE_Route53_CertificateDomain)\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_Route53_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\", \"(STAGING) Pseudo Plum E5\", \"(STAGING) False Fennel E6\", \"(STAGING) Puzzling Parsnip E7\", \"(STAGING) Mysterious Mulberry E8\", \"(STAGING) Fake Fig E9\", \"(STAGING) Counterfeit Cashew R10\", \"(STAGING) Wannabe Watercress R11\", \"(STAGING) Riddling Rhubarb R12\", \"(STAGING) Tenuous Tomato R13\", \"(STAGING) Not Nectarine R14\")\n$LE_Route53_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n###############################################################################\n# Helpers\n###############################################################################\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\n###############################################################################\n# Functions\n###############################################################################\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($OctopusParameters[\"LE_Route53_Use_Staging\"] -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n # Clobber account if it exists.\n $le_account = Get-PAAccount\n if ($le_account) {\n Remove-PAAccount $le_account.Id -Force\n }\n\n $aws_secret_key = ConvertTo-SecureString -String $OctopusParameters[\"LE_Route53_AWSAccount.SecretKey\"] -AsPlainText -Force\n $route53_params = @{\n R53AccessKey = $OctopusParameters[\"LE_Route53_AWSAccount.AccessKey\"];\n R53SecretKey = $aws_secret_key\n }\n\n try {\n $DnsPlugins = @(\"Route53\")\n $DomainList = @($LE_Route53_CertificateDomain)\n \n # If domain is a wildcard e.g. *.example-domain.com, check if a SAN has been requested e.g. example-domain.com.\n if ($LE_Route53_CertificateDomain -match \"\\*.\" -and $OctopusParameters[\"LE_Route53_CreateWildcardSAN\"] -eq $True) {\n $LE_Route53_Certificate_SAN = $LE_Route53_CertificateDomain.Replace(\"*.\",\"\")\n $DomainList += $LE_Route53_Certificate_SAN\n # Include additional DnsPlugin of same type to surpress warning.\n $DnsPlugins += \"Route53\"\n }\n\n $Cert_Params = @{\n Domain = $DomainList\n AcceptTOS = $True;\n Contact = $OctopusParameters[\"LE_Route53_ContactEmailAddress\"];\n DnsPlugin = $DnsPlugins;\n PluginArgs = $route53_params;\n PfxPass = $OctopusParameters[\"LE_Route53_PfxPassword\"];\n Force = $True;\n }\n\n return New-PACertificate @Cert_Params\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Route53_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$($LE_Route53_CertificateDomain)\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_Route53_Issuers\n if ($OctopusParameters[\"LE_Route53_Use_Staging\"] -eq $True) {\n $possible_issuers = $LE_Route53_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_Route53_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Route53_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $($LE_Route53_CertificateDomain) certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $($LE_Route53_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n exit 1\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $OctopusParameters[\"LE_Route53_Octopus_APIKey\"] }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $($LE_Route53_CertificateDomain) certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $($LE_Route53_CertificateDomain) certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n exit 1\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_Route53_CertificateName\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $OctopusParameters[\"LE_Route53_PfxPassword\"];\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit 1\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($Certificate.PfxFullChain)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $OctopusParameters[\"LE_Route53_PfxPassword\"];\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n###############################################################################\n# DO THE THING | MAIN |\n###############################################################################\nWrite-Debug \"Do the Thing\"\n\nWrite-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store.\"\n$certificates = Get-OctopusCertificates\n\n# Check for PFX & PEM\nif ($certificates) {\n\n # Handle weird behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $($LE_Route53_CertificateDomain).\"\n Write-Host \"Checking to see if any expire within $($OctopusParameters[\"LE_Route53_ReplaceIfExpiresInDays\"]) days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($OctopusParameters[\"LE_Route53_ReplaceIfExpiresInDays\"]) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $($OctopusParameters[\"LE_Route53_ReplaceIfExpiresInDays\"]) days. Requesting new certificates for $($LE_Route53_CertificateDomain) from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n exit 0\n}\n\n# No existing Certificates - Lets get some new ones.\nWrite-Host \"No existing certificates found for $($LE_Route53_CertificateDomain).\"\nWrite-Host \"Request New Certificate for $($LE_Route53_CertificateDomain) from Lets Encrypt\"\n\n# New Certificate..\n$le_certificate = Get-LetsEncryptCertificate\n\nWrite-Host \"Publishing: LetsEncrypt - $($LE_Route53_CertificateDomain) (PFX)\"\n$certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\nPublish-OctopusCertificate -JsonBody $certificate_as_json\n\nWrite-Host \"GREAT SUCCESS\"\n", "Octopus.Action.SubstituteInFiles.Enabled": "True" }, "Parameters": [{ @@ -92,12 +92,11 @@ } } ], - "LastModifiedAt": "2022-02-07T09:38:11.788Z", "$Meta": { - "ExportedAt": "2024-06-24T06:57:36.821Z", - "OctopusVersion": "2024.3.4152", + "ExportedAt": "2024-08-01T10:57:00.608Z", + "OctopusVersion": "2024.3.8336", "Type": "ActionTemplate" }, - "LastModifiedBy": "benjimac93", + "LastModifiedBy": "harrisonmeister", "Category": "lets-encrypt" } diff --git a/step-templates/letsencrypt-selfhosted-http.json b/step-templates/letsencrypt-selfhosted-http.json index 49d7c3e24..467f021a3 100644 --- a/step-templates/letsencrypt-selfhosted-http.json +++ b/step-templates/letsencrypt-selfhosted-http.json @@ -3,13 +3,13 @@ "Name": "Lets Encrypt - Self-Hosted HTTP Challenge", "Description": "Request (or renew) an X.509 SSL Certificate from the [Let's Encrypt Certificate Authority](https://letsencrypt.org/) using the Self-hosted HTTP Challenge Listener provided by the [Posh-ACME](https://github.com/rmbolger/Posh-ACME/) PowerShell Module.\n\n---\n#### Please Note\n\nIt's generally a better idea to use one of the Posh-ACME [DNS providers](https://github.com/rmbolger/Posh-ACME/wiki/List-of-Supported-DNS-Providers) for Let's Encrypt.\n\nThere are a number of Octopus Step templates in the [Community Library](https://library.octopus.com/listing/letsencrypt) that support DNS providers.\n\n---\n\n#### Features\n\n- ACME v2 protocol support which allows generating wildcard certificates (*.example.com).\n- [Self-hosted HTTP Challenge](https://github.com/rmbolger/Posh-ACME/wiki/How-To-Self-Host-HTTP-Challenges) Challenge for TLD, CNAME, and Wildcard domains. \n- _Optionally_ Publishes/Updates SSL Certificates in the [Octopus Deploy Certificate Store](https://octopus.com/docs/deployment-examples/certificates).\n- _Optionally_ import SSL Certificate into the local machine store. \n- _Optionally_ Export PFX (PKCS#12) SSL Certificate to a supplied file path.\n- Verified to work on Windows and Linux deployment targets\n\n#### Pre-requisites\n\n- There are specific requirements when [running on Windows](https://github.com/rmbolger/Posh-ACME/wiki/How-To-Self-Host-HTTP-Challenges#windows-only-prerequisites).\n- HTTP Challenge Listener must be available on Port 80.\n- When updating the Octopus Certificate Store, access to the Octopus Server from where the script template runs e.g. deployment target or worker is required.", "ActionType": "Octopus.Script", - "Version": 10, + "Version": 11, "CommunityActionTemplateId": null, "Packages": [], "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "# TLS 1.2\nWrite-Host \"Enabling TLS 1.2 for script execution\"\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nWrite-Host \"Importing Posh-ACME\"\nImport-Module Posh-ACME\n\n# Variables\n$LE_SelfHosted_CertificateDomain = $OctopusParameters[\"LE_SelfHosted_CertificateDomain\"]\n$LE_SelfHosted_Contact = $OctopusParameters[\"LE_SelfHosted_ContactEmailAddress\"]\n$LE_SelfHosted_PfxPass = $OctopusParameters[\"LE_SelfHosted_PfxPass\"]\n$LE_SelfHosted_Use_Staging = $OctopusParameters[\"LE_SelfHosted_Use_Staging\"]\n$LE_SelfHosted_HttpListenerTimeout = $OctopusParameters[\"LE_SelfHosted_HttpListenerTimeout\"]\n$LE_Self_Hosted_UpdateOctopusCertificateStore = $OctopusParameters[\"LE_Self_Hosted_UpdateOctopusCertificateStore\"]\n$LE_SelfHosted_Octopus_APIKey = $OctopusParameters[\"LE_SelfHosted_Octopus_APIKey\"]\n$LE_SelfHosted_ReplaceIfExpiresInDays = $OctopusParameters[\"LE_SelfHosted_ReplaceIfExpiresInDays\"]\n$LE_SelfHosted_Install = $OctopusParameters[\"LE_SelfHosted_Install\"]\n$LE_SelfHosted_ExportFilePath = $OctopusParameters[\"LE_SelfHosted_ExportFilePath\"]\n$LE_SelfHosted_Export = -not [System.String]::IsNullOrWhiteSpace($LE_SelfHosted_ExportFilePath)\n$LE_SelfHosted_TempFileLocation=[System.IO.Path]::GetTempFileName()\n\n# Consts\n$LE_SelfHosted_Certificate_Name = \"Lets Encrypt - $LE_SelfHosted_CertificateDomain\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_SelfHosted_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\")\n$LE_SelfHosted_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n# Helper(s)\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\nfunction Clean-TempFiles {\n\tif(Test-Path -Path $LE_SelfHosted_TempFileLocation) {\n\t\tWrite-Debug \"Removing temporary file...\"\n\t\tRemove-Item $LE_SelfHosted_TempFileLocation -Force\n\t}\n}\n\nfunction Exit-Failure {\n \tClean-TempFiles\n\tExit 1\n}\n\nfunction Exit-Success {\n \tClean-TempFiles\n\tExit 0\n}\n\n# Functions\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($LE_SelfHosted_Use_Staging -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n $le_account = Get-PAAccount\n if ($le_account) {\n Write-Host \"Removing existing PA-Account...\"\n Remove-PAAccount $le_account.Id -Force\n }\n \n Write-Host \"Assigning new PA-Account...\"\n $le_account = New-PAAccount -Contact $LE_SelfHosted_Contact -AcceptTOS -Force\n \n Write-Host \"Requesting new order for $LE_SelfHosted_CertificateDomain...\"\n $order = New-PAOrder -Domain $LE_SelfHosted_CertificateDomain -PfxPass $LE_SelfHosted_PfxPass -Force\n \n try {\n \tWrite-Host \"Invoking Self-Hosted HttpChallengeListener with timeout of $LE_SelfHosted_HttpListenerTimeout seconds...\"\n \tInvoke-HttpChallengeListener -Verbose -ListenerTimeout $LE_SelfHosted_HttpListenerTimeout\n \t\n Write-Host \"Getting validated certificate...\"\n $pArgs = @{ManualNonInteractive=$True}\n $cert = New-PACertificate $LE_SelfHosted_CertificateDomain -PluginArgs $pArgs\n \n if ($LE_SelfHosted_Install -eq $True) {\n \tif (-not $IsWindows -and 'Desktop' -ne $PSEdition) {\n Write-Host \"Installing certificate currently only works on Windows\"\n \t}\n else {\n Write-Host \"Installing certificate to local store...\"\n $cert | Install-PACertificate\n }\n \t}\n \n # Linux showed weird $null issues using the .PfxFullChain path\n if(Test-Path -Path $LE_SelfHosted_TempFileLocation) {\n \tWrite-Debug \"Creating temp copy of certificate to: $LE_SelfHosted_TempFileLocation\"\n \t$bytes = [System.IO.File]::ReadAllBytes($cert.PfxFullChain)\n New-Item -Path $LE_SelfHosted_TempFileLocation -ItemType \"file\" -Force\n [System.IO.File]::WriteAllBytes($LE_SelfHosted_TempFileLocation, $bytes)\n }\n \n if($LE_SelfHosted_Export -eq $True) {\n \tWrite-Host \"Exporting certificate to: $LE_SelfHosted_ExportFilePath\"\n \t$bytes = [System.IO.File]::ReadAllBytes($LE_SelfHosted_TempFileLocation)\n New-Item -Path $LE_SelfHosted_ExportFilePath -ItemType \"file\" -Force\n [System.IO.File]::WriteAllBytes($LE_SelfHosted_ExportFilePath, $bytes)\n \t}\n\n return $cert\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n Exit-Failure\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $LE_SelfHosted_Octopus_APIKey }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$LE_SelfHosted_CertificateDomain\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_SelfHosted_Issuers\n if ($LE_SelfHosted_Use_Staging -eq $True) {\n $possible_issuers = $LE_SelfHosted_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_SelfHosted_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n Exit-Failure\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n Exit-Failure\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $LE_SelfHosted_Octopus_APIKey }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\tWrite-Verbose \"Preparing to publish to: $octopus_certificates_uri\"\n \n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $LE_SelfHosted_CertificateDomain certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $LE_SelfHosted_CertificateDomain certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n Exit-Failure\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n Exit-Failure\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $LE_SelfHosted_Octopus_APIKey }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $LE_SelfHosted_CertificateDomain certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $LE_SelfHosted_CertificateDomain certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n Exit-Failure\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit-Failure\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($LE_SelfHosted_TempFileLocation)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_SelfHosted_CertificateDomain\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $LE_SelfHosted_PfxPass;\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit-Failure\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($LE_SelfHosted_TempFileLocation)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $LE_SelfHosted_PfxPass;\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n# Main Execution starts here\n\nWrite-Debug \"Running MAIN function...\"\n\nif ($LE_Self_Hosted_UpdateOctopusCertificateStore -eq $True) {\n Write-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store...\"\n $certificates = Get-OctopusCertificates\n\n # Check for PFX & PEM\n if ($certificates) {\n\n # Handle behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $LE_SelfHosted_CertificateDomain.\"\n Write-Host \"Checking to see if any expire within $LE_SelfHosted_ReplaceIfExpiresInDays days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($LE_SelfHosted_ReplaceIfExpiresInDays) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $LE_SelfHosted_ReplaceIfExpiresInDays days. Requesting new certificates for $LE_SelfHosted_CertificateDomain from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n\tWrite-Host \"Completed running...\"\n Exit-Success\n }\n}\n\nWrite-Host \"Requesting New Certificate for $LE_SelfHosted_CertificateDomain from Lets Encrypt\"\n\n$le_certificate = Get-LetsEncryptCertificate\n\nif($LE_Self_Hosted_UpdateOctopusCertificateStore -eq $True) {\n Write-Host \"Publishing new LetsEncrypt - $LE_SelfHosted_CertificateDomain (PFX) to Octopus Certificate Store\"\n $certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\n Publish-OctopusCertificate -JsonBody $certificate_as_json\n} \nelse {\n Write-Host \"Certificate generated...\"\n $le_certificate | fl\n}\n\nWrite-Host \"Completed running...\"\nExit-Success" + "Octopus.Action.Script.ScriptBody": "# TLS 1.2\nWrite-Host \"Enabling TLS 1.2 for script execution\"\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12\n\n###############################################################################\n# Required Modules folder\n###############################################################################\nWrite-Host \"Checking for required powershell modules folder\"\n$ModulesFolder = \"$HOME\\Documents\\WindowsPowerShell\\Modules\"\nif ($PSEdition -eq \"Core\") {\n if ($PSVersionTable.Platform -eq \"Unix\") {\n $ModulesFolder = \"$HOME/.local/share/powershell/Modules\"\n }\n else {\n $ModulesFolder = \"$HOME\\Documents\\PowerShell\\Modules\"\n }\n}\n$PSModuleFolderExists = (Test-Path $ModulesFolder)\nif ($PSModuleFolderExists -eq $False) {\n\tWrite-Host \"Creating directory: $ModulesFolder\"\n\tNew-Item $ModulesFolder -ItemType Directory -Force\n $env:PSModulePath = $ModulesFolder + [System.IO.Path]::PathSeparator + $env:PSModulePath\n}\n\n###############################################################################\n# Required Modules\n###############################################################################\nWrite-Host \"Checking for required modules.\"\n$required_posh_acme_version = 3.12.0\n$module_check = Get-Module -ListAvailable -Name Posh-Acme | Where-Object { $_.Version -ge $required_posh_acme_version }\n\nif (-not ($module_check)) {\n Write-Host \"Ensuring NuGet provider is bootstrapped.\"\n Get-PackageProvider NuGet -ForceBootstrap | Out-Null\n Write-Host \"Installing Posh-ACME.\"\n Install-Module -Name Posh-ACME -MinimumVersion 3.12.0 -Scope CurrentUser -Force\n}\n\nWrite-Host \"Importing Posh-ACME\"\nImport-Module Posh-ACME\n\n# Variables\n$LE_SelfHosted_CertificateDomain = $OctopusParameters[\"LE_SelfHosted_CertificateDomain\"]\n$LE_SelfHosted_Contact = $OctopusParameters[\"LE_SelfHosted_ContactEmailAddress\"]\n$LE_SelfHosted_PfxPass = $OctopusParameters[\"LE_SelfHosted_PfxPass\"]\n$LE_SelfHosted_Use_Staging = $OctopusParameters[\"LE_SelfHosted_Use_Staging\"]\n$LE_SelfHosted_HttpListenerTimeout = $OctopusParameters[\"LE_SelfHosted_HttpListenerTimeout\"]\n$LE_Self_Hosted_UpdateOctopusCertificateStore = $OctopusParameters[\"LE_Self_Hosted_UpdateOctopusCertificateStore\"]\n$LE_SelfHosted_Octopus_APIKey = $OctopusParameters[\"LE_SelfHosted_Octopus_APIKey\"]\n$LE_SelfHosted_ReplaceIfExpiresInDays = $OctopusParameters[\"LE_SelfHosted_ReplaceIfExpiresInDays\"]\n$LE_SelfHosted_Install = $OctopusParameters[\"LE_SelfHosted_Install\"]\n$LE_SelfHosted_ExportFilePath = $OctopusParameters[\"LE_SelfHosted_ExportFilePath\"]\n$LE_SelfHosted_Export = -not [System.String]::IsNullOrWhiteSpace($LE_SelfHosted_ExportFilePath)\n$LE_SelfHosted_TempFileLocation=[System.IO.Path]::GetTempFileName()\n\n# Consts\n$LE_SelfHosted_Certificate_Name = \"Lets Encrypt - $LE_SelfHosted_CertificateDomain\"\n\n# Issuer used in a cert could be one of multiple, including ones no longer supported by Let's Encrypt\n$LE_SelfHosted_Fake_Issuers = @(\"Fake LE Intermediate X1\", \"(STAGING) Artificial Apricot R3\", \"(STAGING) Ersatz Edamame E1\", \"(STAGING) Pseudo Plum E5\", \"(STAGING) False Fennel E6\", \"(STAGING) Puzzling Parsnip E7\", \"(STAGING) Mysterious Mulberry E8\", \"(STAGING) Fake Fig E9\", \"(STAGING) Counterfeit Cashew R10\", \"(STAGING) Wannabe Watercress R11\", \"(STAGING) Riddling Rhubarb R12\", \"(STAGING) Tenuous Tomato R13\", \"(STAGING) Not Nectarine R14\")\n$LE_SelfHosted_Issuers = @(\"Let's Encrypt Authority X3\", \"E1\", \"E2\", \"R3\", \"R4\", \"R5\", \"R6\", \"R10\", \"R11\")\n\n# Helper(s)\nfunction Get-WebRequestErrorBody {\n param (\n $RequestError\n )\n\n # Powershell < 6 you can read the Exception\n if ($PSVersionTable.PSVersion.Major -lt 6) {\n if ($RequestError.Exception.Response) {\n $reader = New-Object System.IO.StreamReader($RequestError.Exception.Response.GetResponseStream())\n $reader.BaseStream.Position = 0\n $reader.DiscardBufferedData()\n $response = $reader.ReadToEnd()\n return $response | ConvertFrom-Json\n }\n }\n else {\n return $RequestError.ErrorDetails.Message\n }\n}\n\nfunction Clean-TempFiles {\n\tif(Test-Path -Path $LE_SelfHosted_TempFileLocation) {\n\t\tWrite-Debug \"Removing temporary file...\"\n\t\tRemove-Item $LE_SelfHosted_TempFileLocation -Force\n\t}\n}\n\nfunction Exit-Failure {\n \tClean-TempFiles\n\tExit 1\n}\n\nfunction Exit-Success {\n \tClean-TempFiles\n\tExit 0\n}\n\n# Functions\nfunction Get-LetsEncryptCertificate {\n Write-Debug \"Entering: Get-LetsEncryptCertificate\"\n\n if ($LE_SelfHosted_Use_Staging -eq $True) {\n Write-Host \"Using Lets Encrypt Server: Staging\"\n Set-PAServer LE_STAGE;\n }\n else {\n Write-Host \"Using Lets Encrypt Server: Production\"\n Set-PAServer LE_PROD;\n }\n\n $le_account = Get-PAAccount\n if ($le_account) {\n Write-Host \"Removing existing PA-Account...\"\n Remove-PAAccount $le_account.Id -Force\n }\n \n Write-Host \"Assigning new PA-Account...\"\n $le_account = New-PAAccount -Contact $LE_SelfHosted_Contact -AcceptTOS -Force\n \n Write-Host \"Requesting new order for $LE_SelfHosted_CertificateDomain...\"\n $order = New-PAOrder -Domain $LE_SelfHosted_CertificateDomain -PfxPass $LE_SelfHosted_PfxPass -Force\n \n try {\n \tWrite-Host \"Invoking Self-Hosted HttpChallengeListener with timeout of $LE_SelfHosted_HttpListenerTimeout seconds...\"\n \tInvoke-HttpChallengeListener -Verbose -ListenerTimeout $LE_SelfHosted_HttpListenerTimeout\n \t\n Write-Host \"Getting validated certificate...\"\n $pArgs = @{ManualNonInteractive=$True}\n $cert = New-PACertificate $LE_SelfHosted_CertificateDomain -PluginArgs $pArgs\n \n if ($LE_SelfHosted_Install -eq $True) {\n \tif (-not $IsWindows -and 'Desktop' -ne $PSEdition) {\n Write-Host \"Installing certificate currently only works on Windows\"\n \t}\n else {\n Write-Host \"Installing certificate to local store...\"\n $cert | Install-PACertificate\n }\n \t}\n \n # Linux showed weird $null issues using the .PfxFullChain path\n if(Test-Path -Path $LE_SelfHosted_TempFileLocation) {\n \tWrite-Debug \"Creating temp copy of certificate to: $LE_SelfHosted_TempFileLocation\"\n \t$bytes = [System.IO.File]::ReadAllBytes($cert.PfxFullChain)\n New-Item -Path $LE_SelfHosted_TempFileLocation -ItemType \"file\" -Force\n [System.IO.File]::WriteAllBytes($LE_SelfHosted_TempFileLocation, $bytes)\n }\n \n if($LE_SelfHosted_Export -eq $True) {\n \tWrite-Host \"Exporting certificate to: $LE_SelfHosted_ExportFilePath\"\n \t$bytes = [System.IO.File]::ReadAllBytes($LE_SelfHosted_TempFileLocation)\n New-Item -Path $LE_SelfHosted_ExportFilePath -ItemType \"file\" -Force\n [System.IO.File]::WriteAllBytes($LE_SelfHosted_ExportFilePath, $bytes)\n \t}\n\n return $cert\n }\n catch {\n Write-Host \"Failed to Create Certificate. Error Message: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n Exit-Failure\n }\n}\n\nfunction Get-OctopusCertificates {\n Write-Debug \"Entering: Get-OctopusCertificates\"\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $LE_SelfHosted_Octopus_APIKey }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates?search=$LE_SelfHosted_CertificateDomain\"\n\n try {\n # Get a list of certificates that match our domain search criteria.\n $certificates_search = Invoke-WebRequest -Uri $octopus_certificates_uri -Method Get -Headers $octopus_headers -UseBasicParsing -ErrorAction Stop | ConvertFrom-Json | Select-Object -ExpandProperty Items\n\n # We don't want to confuse Production and Staging Lets Encrypt Certificates.\n $possible_issuers = $LE_SelfHosted_Issuers\n if ($LE_SelfHosted_Use_Staging -eq $True) {\n $possible_issuers = $LE_SelfHosted_Fake_Issuers\n }\n\n return $certificates_search | Where-Object {\n $_.SubjectCommonName -eq $LE_SelfHosted_CertificateDomain -and\n $possible_issuers -contains $_.IssuerCommonName -and\n $null -eq $_.ReplacedBy -and\n $null -eq $_.Archived\n }\n }\n catch {\n Write-Host \"Could not retrieve certificates from Octopus Deploy. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n Exit-Failure\n }\n}\n\nfunction Publish-OctopusCertificate {\n param (\n [string] $JsonBody\n )\n\n Write-Debug \"Entering: Publish-OctopusCertificate\"\n\n if (-not ($JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n Exit-Failure\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $LE_SelfHosted_Octopus_APIKey }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates\"\n\tWrite-Verbose \"Preparing to publish to: $octopus_certificates_uri\"\n \n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Published $LE_SelfHosted_CertificateDomain certificate to the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Host \"Failed to publish $LE_SelfHosted_CertificateDomain certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n Exit-Failure\n }\n}\n\nfunction Update-OctopusCertificate {\n param (\n [string]$Certificate_Id,\n [string]$JsonBody\n )\n\n Write-Debug \"Entering: Update-OctopusCertificate\"\n\n if (-not ($Certificate_Id -and $JsonBody)) {\n Write-Host \"Existing Certificate Id and a replace Certificate are required.\"\n Exit-Failure\n }\n\n $octopus_uri = $OctopusParameters[\"Octopus.Web.ServerUri\"]\n $octopus_space_id = $OctopusParameters[\"Octopus.Space.Id\"]\n $octopus_headers = @{ \"X-Octopus-ApiKey\" = $LE_SelfHosted_Octopus_APIKey }\n $octopus_certificates_uri = \"$octopus_uri/api/$octopus_space_id/certificates/$Certificate_Id/replace\"\n\n try {\n Invoke-WebRequest -Uri $octopus_certificates_uri -Method Post -Headers $octopus_headers -Body $JsonBody -UseBasicParsing\n Write-Host \"Replaced $LE_SelfHosted_CertificateDomain certificate in the Octopus Deploy Certificate Store.\"\n }\n catch {\n Write-Error \"Failed to replace $LE_SelfHosted_CertificateDomain certificate. Error: $($_.Exception.Message). See Debug output for details.\"\n Write-Debug (Get-WebRequestErrorBody -RequestError $_)\n Exit-Failure\n }\n}\n\nfunction Get-NewCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-NewCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit-Failure\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($LE_SelfHosted_TempFileLocation)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n Name = \"$LE_SelfHosted_CertificateDomain\";\n Notes = \"\";\n CertificateData = @{\n HasValue = $true;\n NewValue = $certificate_base64;\n };\n Password = @{\n HasValue = $true;\n NewValue = $LE_SelfHosted_PfxPass;\n };\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\nfunction Get-ReplaceCertificatePFXAsJson {\n param (\n $Certificate\n )\n\n Write-Debug \"Entering: Get-ReplaceCertificatePFXAsJson\"\n\n if (-not ($Certificate)) {\n Write-Host \"Certificate is required.\"\n Exit-Failure\n }\n\n [Byte[]]$certificate_buffer = [System.IO.File]::ReadAllBytes($LE_SelfHosted_TempFileLocation)\n $certificate_base64 = [convert]::ToBase64String($certificate_buffer)\n\n $certificate_body = @{\n CertificateData = $certificate_base64;\n Password = $LE_SelfHosted_PfxPass;\n }\n\n return $certificate_body | ConvertTo-Json\n}\n\n# Main Execution starts here\n\nWrite-Debug \"Running MAIN function...\"\n\nif ($LE_Self_Hosted_UpdateOctopusCertificateStore -eq $True) {\n Write-Host \"Checking for existing Lets Encrypt Certificates in the Octopus Deploy Certificates Store...\"\n $certificates = Get-OctopusCertificates\n\n # Check for PFX & PEM\n if ($certificates) {\n\n # Handle behavior between Powershell 5 and Powershell 6+\n $certificate_count = 1\n if ($certificates.Count -ge 1) {\n $certificate_count = $certificates.Count\n }\n\n Write-Host \"Found $certificate_count for $LE_SelfHosted_CertificateDomain.\"\n Write-Host \"Checking to see if any expire within $LE_SelfHosted_ReplaceIfExpiresInDays days.\"\n\n # Check Expiry Dates\n $expiring_certificates = $certificates | Where-Object { [DateTime]$_.NotAfter -lt (Get-Date).AddDays($LE_SelfHosted_ReplaceIfExpiresInDays) }\n\n if ($expiring_certificates) {\n Write-Host \"Found certificates that expire with $LE_SelfHosted_ReplaceIfExpiresInDays days. Requesting new certificates for $LE_SelfHosted_CertificateDomain from Lets Encrypt\"\n $le_certificate = Get-LetsEncryptCertificate\n\n # PFX\n $existing_certificate = $certificates | Where-Object { $_.CertificateDataFormat -eq \"Pkcs12\" } | Select-Object -First 1\n $certificate_as_json = Get-ReplaceCertificatePFXAsJson -Certificate $le_certificate\n Update-OctopusCertificate -Certificate_Id $existing_certificate.Id -JsonBody $certificate_as_json\n }\n else {\n Write-Host \"Nothing to do here...\"\n }\n\n\tWrite-Host \"Completed running...\"\n Exit-Success\n }\n}\n\nWrite-Host \"Requesting New Certificate for $LE_SelfHosted_CertificateDomain from Lets Encrypt\"\n\n$le_certificate = Get-LetsEncryptCertificate\n\nif($LE_Self_Hosted_UpdateOctopusCertificateStore -eq $True) {\n Write-Host \"Publishing new LetsEncrypt - $LE_SelfHosted_CertificateDomain (PFX) to Octopus Certificate Store\"\n $certificate_as_json = Get-NewCertificatePFXAsJson -Certificate $le_certificate\n Publish-OctopusCertificate -JsonBody $certificate_as_json\n} \nelse {\n Write-Host \"Certificate generated...\"\n $le_certificate | fl\n}\n\nWrite-Host \"Completed running...\"\nExit-Success" }, "Parameters": [ { @@ -113,12 +113,11 @@ } } ], - "LastModifiedAt": "2022-02-07T09:38:11.788Z", "$Meta": { - "ExportedAt": "2024-06-24T06:57:36.821Z", - "OctopusVersion": "2024.3.4152", + "ExportedAt": "2024-08-01T10:57:00.608Z", + "OctopusVersion": "2024.3.8336", "Type": "ActionTemplate" }, - "LastModifiedBy": "benjimac93", + "LastModifiedBy": "harrisonmeister", "Category": "lets-encrypt" }