Skip to content

Commit f1adca6

Browse files
Enh ssl module (#267)
* Initial commit * Functional component * Adding Pester tests * Adding tests to new resource * Had some issues in tests * Fixed linting errors and added example * Fixed tests for all authentication methods. The global variable of '' seemed to be unavailable in the BeforeAll, added path resolution here and it worked. * Implementing suggested changes. * More changes * Revert "Fixed tests for all authentication methods. The global variable of '' seemed to be unavailable in the BeforeAll, added path resolution here and it worked." This reverts commit 6beee01. * Implementing suggested changes. * Changing cOctopusServerSslCertificate.Tests from Context to Describe blocks. * Adding readme for new resource * Adding referenced to new readme from main readme * Adding e2e tests for OctopusServerSslCertificate resource. * Aligning code, removed commented out code. * Update OctopusDSC/DSCResources/cOctopusServerSslCertificate/cOctopusServerSslCertificate.psm1 Co-authored-by: Matt Richardson <matt.richardson@octopus.com> * Update OctopusDSC/DSCResources/cOctopusServerSslCertificate/cOctopusServerSslCertificate.psm1 Co-authored-by: Matt Richardson <matt.richardson@octopus.com> * Update OctopusDSC/Tests/cOctopusServerSslCertificate.Tests.ps1 Co-authored-by: Matt Richardson <matt.richardson@octopus.com> * Update README-cOctopusServerSslCertificate.md Co-authored-by: Matt Richardson <matt.richardson@octopus.com> * Update README-cOctopusServerSslCertificate.md Co-authored-by: Matt Richardson <matt.richardson@octopus.com> * Update OctopusDSC/Examples/cOctopusServerSslCertificate.ps1 Co-authored-by: Matt Richardson <matt.richardson@octopus.com> Co-authored-by: Matt Richardson <matt.richardson@octopus.com>
1 parent fc0eef8 commit f1adca6

11 files changed

+403
-3
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# dot-source the helper file (cannot load as a module due to scope considerations)
2+
. (Join-Path -Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -ChildPath 'OctopusDSCHelpers.ps1')
3+
4+
# This is the Octopus Deploy server Application ID, declared within Octopus Server itself
5+
$octopusServerApplicationId = "{E2096A4C-2391-4BE1-9F17-E353F930E7F1}"
6+
7+
function Get-CurrentSSLBinding {
8+
param([string] $ApplicationId,
9+
[string]$Port)
10+
11+
$certificateBindings = (& netsh http show sslcert) | select-object -skip 3 | out-string
12+
$newLine = [System.Environment]::NewLine
13+
$certificateBindings = $certificateBindings -split "$newLine$newLine"
14+
$certificateBindingsList = foreach ($certificateBinding in $certificateBindings) {
15+
if ($certificateBinding -ne "") {
16+
$certificateBinding = $certificateBinding -replace " ", "" -split ": "
17+
[pscustomobject]@{
18+
IPPort = ($certificateBinding[1] -split "`n")[0]
19+
CertificateThumbprint = ($certificateBinding[2] -split "`n" -replace '[^a-zA-Z0-9]', '')[0]
20+
AppID = ($certificateBinding[3] -split "`n")[0]
21+
CertStore = ($certificateBinding[4] -split "`n")[0]
22+
}
23+
}
24+
}
25+
26+
return ($certificateBindingsList | Where-Object {($_.AppID.Trim() -eq $ApplicationId) -and ($_.IPPort.Trim() -eq "{0}:{1}" -f "0.0.0.0", $Port) })
27+
}
28+
29+
function Get-TargetResource {
30+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")]
31+
param (
32+
[Parameter(Mandatory)]
33+
[string] $InstanceName,
34+
[ValidateSet("Present", "Absent")]
35+
[string] $Ensure = "Present",
36+
[ValidateNotNullOrEmpty()]
37+
[Parameter(Mandatory)]
38+
[string] $Thumbprint,
39+
[ValidateNotNullOrEmpty()]
40+
[Parameter(Mandatory)]
41+
[string] $StoreName,
42+
[ValidateNotNullOrEmpty()]
43+
[int] $Port = 443
44+
)
45+
46+
$existingSSLConfig = (Get-CurrentSSLBinding -ApplicationId $octopusServerApplicationId -Port $Port -IPAddress $IPAddress)
47+
48+
if ($null -ne $existingSSLConfig) {
49+
$existingSSLPort = [int](($existingSSLConfig.IPPort).Split(":")[1])
50+
$existingSSLThumbprint = $existingSSLConfig.CertificateThumbprint.Trim()
51+
$existingSSLCertificateStoreName = $existingSSLConfig.CertStore.Trim()
52+
$existingEnsure = "Present"
53+
}
54+
else {
55+
$existingEnsure = "Absent"
56+
}
57+
58+
$result = @{
59+
InstanceName = $InstanceName
60+
Ensure = $existingEnsure
61+
Port = $existingSSLPort
62+
StoreName = $existingSSLCertificateStoreName
63+
Thumbprint = $existingSSLThumbprint
64+
}
65+
66+
return $result
67+
}
68+
69+
function Test-TargetResource {
70+
param(
71+
[Parameter(Mandatory)]
72+
[string] $InstanceName,
73+
[ValidateSet("Present", "Absent")]
74+
[string] $Ensure = "Present",
75+
[ValidateNotNullOrEmpty()]
76+
[Parameter(Mandatory)]
77+
[string] $Thumbprint,
78+
[ValidateNotNullOrEmpty()]
79+
[Parameter(Mandatory)]
80+
[string] $StoreName,
81+
[ValidateNotNullOrEmpty()]
82+
[int] $Port = 443
83+
)
84+
85+
$currentResource = (Get-TargetResource @PSBoundParameters)
86+
$params = Get-ODSCParameter $MyInvocation.MyCommand.Parameters
87+
88+
$currentConfigurationMatchesRequestedConfiguration = $true
89+
foreach ($key in $currentResource.Keys) {
90+
$currentValue = $currentResource.Item($key)
91+
$requestedValue = $params.Item($key)
92+
93+
if ($currentValue -ne $requestedValue) {
94+
Write-Verbose "(FOUND MISMATCH) Configuration parameter '$key' with value '$currentValue' mismatched the specified value '$requestedValue'"
95+
$currentConfigurationMatchesRequestedConfiguration = $false
96+
}
97+
else {
98+
Write-Verbose "Configuration parameter '$key' matches the requested value '$requestedValue'"
99+
}
100+
}
101+
102+
return $currentConfigurationMatchesRequestedConfiguration
103+
}
104+
105+
function Set-TargetResource {
106+
param(
107+
[Parameter(Mandatory)]
108+
[string] $InstanceName,
109+
[ValidateSet("Present", "Absent")]
110+
[string] $Ensure = "Present",
111+
[ValidateNotNullOrEmpty()]
112+
[Parameter(Mandatory)]
113+
[string] $Thumbprint,
114+
[ValidateNotNullOrEmpty()]
115+
[Parameter(Mandatory)]
116+
[string] $StoreName,
117+
[ValidateNotNullOrEmpty()]
118+
[int] $Port = 443
119+
)
120+
121+
if ($Ensure -eq "Present") {
122+
$exeArgs = @(
123+
'ssl-certificate',
124+
'--instance', $Instancename,
125+
'--thumbprint', $Thumbprint,
126+
'--certificate-store', $StoreName,
127+
'--port', $Port
128+
)
129+
130+
Write-Verbose "Binding certificate ..."
131+
Invoke-OctopusServerCommand $exeArgs
132+
}
133+
else {
134+
$currentBinding = (Get-CurrentSSLBinding -ApplicationId $octopusServerApplicationId -Port $Port -IPAddress $IPAddress)
135+
if ($null -ne $currentBinding) {
136+
Write-Verbose "Removing certificate binding ..."
137+
# ideally, we'd call a command in Octopus.Server for this, but there's no command to do this (at this point)
138+
& netsh http delete sslcert ("{0}:{1}" -f "0.0.0.0", $Port)
139+
$currentBinding = (Get-CurrentSSLBinding -ApplicationId $octopusServerApplicationId -Port $Port -IPAddress $IPAddress)
140+
141+
if ($null -eq $currentBinding) {
142+
Write-Verbose "Binding successfully removed."
143+
}
144+
}
145+
}
146+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[ClassVersion("1.0.0"), FriendlyName("cOctopusServerSslCertificate")]
2+
class cOctopusServerSslCertificate : OMI_BaseResource
3+
{
4+
[Key, Description("Name of the Octopus Server instance")] string InstanceName;
5+
[Write, Required, Description("Thumbprint of the certificate")] string Thumbprint;
6+
[Write, ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] string Ensure;
7+
[Write, Required, ValueMap{"My", "WebHosting"}, Values{"My", "WebHosting"}] string StoreName;
8+
[Write] uint16 Port;
9+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Binds an SSL certificate to the Octopus Server to allow Octopus to listen over https
2+
3+
Configuration SampleConfig
4+
{
5+
Import-DscResource -Module OctopusDSC
6+
7+
Node "localhost"
8+
{
9+
cOctopusServerSslCertificate "Bind SSL Certifiate"
10+
{
11+
InstanceName = "OctopusServer"
12+
Thumbprint = "c42a148bcd3959101f2e7a3d76edb924bff84b6b"
13+
Ensure = "Present"
14+
StoreName = "My"
15+
Port = 443
16+
}
17+
}
18+
}

OctopusDSC/OctopusDSC.psd1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ CmdletsToExport = @()
7373
AliasesToExport = @()
7474

7575
# DSC resources to export from this module
76-
DscResourcesToExport = @('cOctopusEnvironment', 'cOctopusSeqLogger', 'cOctopusServer', 'cOctopusServerActiveDirectoryAuthentication', 'cOctopusServerAzureADAuthentication', 'cOctopusServerGoogleAppsAuthentication', 'cOctopusServerGuestAuthentication', 'cOctopusServerOktaAuthentication', 'cOctopusServerSpace', 'cOctopusServerUsernamePasswordAuthentication', 'cOctopusServerWatchdog', 'cOctopusWorkerPool', 'cTentacleAgent', 'cTentacleWatchdog')
76+
DscResourcesToExport = @('cOctopusEnvironment', 'cOctopusSeqLogger', 'cOctopusServer', 'cOctopusServerActiveDirectoryAuthentication', 'cOctopusServerAzureADAuthentication', 'cOctopusServerGoogleAppsAuthentication', 'cOctopusServerGuestAuthentication', 'cOctopusServerOktaAuthentication', 'cOctopusServerSpace', 'cOctopusServerUsernamePasswordAuthentication', 'cOctopusServerWatchdog', 'cOctopusWorkerPool', 'cTentacleAgent', 'cTentacleWatchdog', 'cOctopusServerSslCertificate')
7777

7878
# List of all modules packaged with this module
7979
# ModuleList = @()
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#requires -Version 4.0
2+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] # these are tests, not anything that needs to be secure
3+
param()
4+
5+
$moduleName = Split-Path ($PSCommandPath -replace '\.Tests\.ps1$', '') -Leaf
6+
$modulePath = Split-Path $PSCommandPath -Parent
7+
$modulePath = Resolve-Path "$PSCommandPath/../../DSCResources/$moduleName/$moduleName.psm1"
8+
$module = $null
9+
10+
try
11+
{
12+
$prefix = [guid]::NewGuid().Guid -replace '-'
13+
$module = Import-Module $modulePath -Prefix $prefix -PassThru -ErrorAction Stop
14+
15+
InModuleScope $module.Name {
16+
$sampleConfigPath = Split-Path $PSCommandPath -Parent
17+
$sampleConfigPath = Join-Path $sampleConfigPath "SampleConfigs"
18+
19+
Describe 'cOctopusServerSslCertificate' {
20+
Describe 'Get-TargetResource - when present and port not specified' {
21+
It 'Returns correct data when SSL binding exists' {
22+
Mock Get-CurrentSSLBinding {
23+
return [PSCustomObject]@{
24+
IPPort = "0.0.0.0:443"
25+
CertificateThumbprint = "dfcbdb879e5315e05982c05793e57c39241797e3"
26+
AppID = "{E2096A4C-2391-4BE1-9F17-E353F930E7F1}"
27+
CertStore = "My"
28+
}
29+
}
30+
31+
$result = Get-TargetResource -InstanceName "OctopusServer" `
32+
-Ensure "Present" `
33+
-Thumbprint "dfcbdb879e5315e05982c05793e57c39241797e3" `
34+
-StoreName "My"
35+
36+
$result.Ensure | Should -Be 'Present'
37+
$result.Port | Should -Be "443"
38+
$result.StoreName | Should -Be "My"
39+
$result.Thumbprint | Should -Be "dfcbdb879e5315e05982c05793e57c39241797e3"
40+
}
41+
}
42+
43+
Describe 'Get-TargetResource - when present and port specified' {
44+
It 'Returns present and 1443 when SSL binding exists' {
45+
Mock Get-CurrentSSLBinding {
46+
return [PSCustomObject]@{
47+
IPPort = "0.0.0.0:1443"
48+
CertificateThumbprint = "dfcbdb879e5315e05982c05793e57c39241797e3"
49+
AppID = "{E2096A4C-2391-4BE1-9F17-E353F930E7F1}"
50+
CertStore = "My"
51+
}
52+
}
53+
54+
$result = Get-TargetResource -InstanceName "OctopusServer" `
55+
-Ensure "Present" `
56+
-Thumbprint "dfcbdb879e5315e05982c05793e57c39241797e3" `
57+
-StoreName "My" `
58+
-Port "1443"
59+
60+
$result.Ensure | Should -Be 'Present'
61+
$result.Port | Should -Be "1443"
62+
$result.StoreName | Should -Be "My"
63+
$result.Thumbprint | Should -Be "dfcbdb879e5315e05982c05793e57c39241797e3"
64+
}
65+
}
66+
67+
Describe 'Get-TargetResource - when absent' {
68+
It 'Returns absent when binding does not exist' {
69+
Mock Get-CurrentSSLBinding {
70+
return $null
71+
}
72+
73+
$result = Get-TargetResource -InstanceName "OctopusServer" `
74+
-Ensure "Present" `
75+
-Thumbprint "dfcbdb879e5315e05982c05793e57c39241797e3" `
76+
-StoreName "My"
77+
78+
$result.Ensure | Should -Be "Absent"
79+
$result.StoreName | Should -Be $null
80+
$result.Thumbprint | Should -Be $null
81+
$result.Port | Should -Be $null
82+
}
83+
}
84+
85+
Describe 'Test-TargetResource - when present' {
86+
It 'Returns false when port differs' {
87+
Mock Get-CurrentSSLBinding {
88+
return [PSCustomObject]@{
89+
IPPort = "0.0.0.0:443"
90+
CertificateThumbprint = "dfcbdb879e5315e05982c05793e57c39241797e3"
91+
AppID = "{E2096A4C-2391-4BE1-9F17-E353F930E7F1}"
92+
CertStore = "My"
93+
}
94+
95+
$result = Test-TargetResource -InstanceName "OctopusServer" `
96+
-Ensure "Present" `
97+
-Thumbprint "dfcbdb879e5315e05982c05793e57c39241797e3" `
98+
-StoreName "My" `
99+
-Port "1443"
100+
101+
$result | Should -Be $false
102+
}
103+
}
104+
105+
It 'Returns true when port matches' {
106+
return [PSCustomObject]@{
107+
IPPort = "0.0.0.0:443"
108+
CertificateThumbprint = "dfcbdb879e5315e05982c05793e57c39241797e3"
109+
AppID = "{E2096A4C-2391-4BE1-9F17-E353F930E7F1}"
110+
CertStore = "My"
111+
}
112+
113+
$result = Test-TargetResource -InstanceName "OctopusServer" `
114+
-Ensure "Present" `
115+
-Thumbprint "dfcbdb879e5315e05982c05793e57c39241797e3" `
116+
-StoreName "My" `
117+
-Port "443"
118+
119+
$result | Should -Be $true
120+
}
121+
}
122+
}
123+
}
124+
}
125+
finally
126+
{
127+
if ($module)
128+
{
129+
Remove-Module -ModuleInfo $module
130+
}
131+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# README-cOctopusServerSslCertificate
2+
3+
This resource binds a SSL certificate to allow Octopus Server to listen over HTTPS.
4+
It binds the certificate stored in the Windows Certificate store identified by the `StoreName` and `Thumbprint` to the port that the sever will listen on.
5+
This port must match the https entry in the `WebListenPrefix` element of the cOctopusServer resource.
6+
7+
## Sample
8+
9+
First, ensure the OctopusDSC module is on your `$env:PSModulePath`. Then you can create and apply configuration like this.
10+
11+
```PowerShell
12+
Configuration SampleConfig
13+
{
14+
Import-DscResource -Module OctopusDSC
15+
16+
Node "localhost"
17+
{
18+
cOctopusServerSslCertificate SSLCert
19+
{
20+
InstanceName = "OctopusServer"
21+
Thumbprint = "<Thumbprint string from certificate>"
22+
Ensure = "Present"
23+
StoreName = "My"
24+
Port = 443
25+
}
26+
}
27+
}
28+
29+
SampleConfig
30+
31+
Start-DscConfiguration .\SampleConfig -Verbose -wait
32+
33+
Test-DscConfiguration
34+
```
35+
36+
## Properties
37+
| Property | Type | Default Value | Description |
38+
| ------------------------------------------| ------------------------------- | -----------------| ------------------------------------------------------------------------------------------------------------------ |
39+
| `Ensure` | `string` - `Present` or `Absent`| `Present` | The desired state of the Octopus Server - effectively whether to install or uninstall. |
40+
| `InstanceName` | `string` | | The name of the Octopus Server instance. Use `OctopusServer` by convention unless you have more than one instance. |
41+
| `Thumbprint` | `string` | | Thumbprint of the SSL certificate to use with Octopus Deploy, |
42+
| `StoreName` | `string` - `My` or `WebHosting` | | Which certificate store to look for the thumbprint in. |
43+
| `Port` | `int` | | The port to use SSL on. |

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Server authentication can be configured to use:
1818

1919
Other resources of note are:
2020
* [cOctopusServerSpace](README-cOctopusServerSpace.md) to manage spaces
21+
* [cOctopusServerSslCertificate](README-OctopusServerSslCertificate) to configure the server to use Ssl with a certificate
2122

2223
Version 3.0 of OctopusDSC supports Octopus Deploy 4.x and above with backwards compatibility to 3.x
2324

0 commit comments

Comments
 (0)