├── .dockerignore
├── .gitignore
├── .vs
└── config
│ └── applicationhost.config
├── Directory.Build.props
├── Dockerfile
├── LetsEncrypt.Azure.DotNetCore.sln
├── examples
└── LetsEncrypt.Azure.FunctionV2
│ ├── .gitignore
│ ├── AutoRenewCertificate.cs
│ ├── Helper.cs
│ ├── LetsEncrypt.Azure.FunctionV2.csproj
│ ├── RequestWildcardCertificate.cs
│ └── host.json
├── media
├── letsencrypt-azure-overiew.png
└── letsencrypt-azure-overiew.vsdx
├── readme.md
└── src
├── LetsEncrypt.Azure.Core.Test
├── AcmeClientTest.cs
├── AzureBlobStorageTest.cs
├── AzureDnsServiceTest.cs
├── GoDaddyDnsProviderTest.cs
├── Letsencrypt.Azure.Core.Test.csproj
├── LoggingTest.cs
├── Properties
│ └── launchSettings.json
├── TestHelper.cs
└── UnoEuroDnsProviderTest.cs
├── LetsEncrypt.Azure.Core.V2
├── AcmeClient.cs
├── AzureBlobStorage.cs
├── AzureHelper.cs
├── CertificateConsumers
│ ├── AzureWebAppService.cs
│ ├── ICertificateConsumer.cs
│ └── NullCertificateConsumer.cs
├── CertificateStores
│ ├── AzureBlobCertificateStore.cs
│ ├── AzureKeyVaultCertificateStore.cs
│ ├── FileSystemBase.cs
│ ├── FileSystemCertificateStore.cs
│ ├── ICertificateStore.cs
│ └── NullCertificateStore.cs
├── DnsLookupService.cs
├── DnsProviders
│ ├── AzureDnsProvider.cs
│ ├── GoDaddyDnsProvider.cs
│ ├── IDnsProvider.cs
│ └── UnoEuroDnsProvider.cs
├── FileSystem.cs
├── IFileSystem.cs
├── LetsEncrypt.Azure.Core.V2.csproj
├── LetsencryptService.cs
├── LetsencryptServiceCollectionExtensions.cs
├── MessageHandler.cs
└── Models
│ ├── AcmeDnsRequest.cs
│ ├── AzureDnsSettings.cs
│ ├── AzureServicePrincipal.cs
│ ├── AzureSubscription.cs
│ ├── AzureWebAppSettings.cs
│ ├── BlobCertificateStoreAppSettings.cs
│ ├── CertificateInfo.cs
│ ├── CertificateInstallModel.cs
│ └── KeyVaultCertificateStoreAppSettings.cs
├── LetsEncrypt.Azure.ResourceGroup.sln
├── LetsEncrypt.Azure.ResourceGroup
├── ArmClient
│ ├── getWebApp.request
│ ├── keyVaultCert.json
│ └── putKeyVaultCertificate.request
├── Deploy-AzureResourceGroup.ps1
├── Deployment.targets
├── LetsEncrypt.Azure.ResourceGroup.deployproj
├── LetsEncrypt.Azure.ResourceGroup.deployproj.user
├── Scripts
│ ├── Create-CoreInfrastructure.ps1
│ ├── Import-Certificate.ps1
│ ├── KeyVault.pfx
│ ├── KeyVault2.pfx
│ ├── KeyVault2_longer-expiration.pfx
│ └── New-Certificate.ps1
├── Templates
│ ├── letsencrypt.functionapp.renewer.json
│ └── letsencrypt.functionapp.renewer.parameters.json
└── bin
│ └── Debug
│ └── staging
│ └── LetsEncrypt.Azure.ResourceGroup
│ ├── Deploy-AzureResourceGroup.ps1
│ └── Templates
│ ├── letsencrypt.azure.core.json
│ └── letsencrypt.azure.core.parameters.json
└── LetsEncrypt.Azure.Runner
├── LetsEncrypt.Azure.Runner.csproj
├── Program.cs
└── Properties
└── launchSettings.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs)
2 | [Bb]in/
3 | [Oo]bj/
4 | node_modules/
5 | dist/
6 |
7 | # mstest test results
8 | TestResults
9 |
10 | ## Ignore Visual Studio temporary files, build results, and
11 | ## files generated by popular Visual Studio add-ons.
12 |
13 | # User-specific files
14 | *.suo
15 | *.user
16 | *.sln.docstates
17 |
18 | # Build results
19 | [Dd]ebug/
20 | [Rr]elease/
21 | x64/
22 | *_i.c
23 | *_p.c
24 | *.ilk
25 | *.meta
26 | *.obj
27 | *.pch
28 | *.pdb
29 | *.pgc
30 | *.pgd
31 | *.rsp
32 | *.sbr
33 | *.tlb
34 | *.tli
35 | *.tlh
36 | *.tmp
37 | *.log
38 | *.vspscc
39 | *.vssscc
40 | .builds
41 |
42 | # Visual C++ cache files
43 | ipch/
44 | *.aps
45 | *.ncb
46 | *.opensdf
47 | *.sdf
48 |
49 | # Visual Studio profiler
50 | *.psess
51 | *.vsp
52 | *.vspx
53 |
54 | # Guidance Automation Toolkit
55 | *.gpState
56 |
57 | # ReSharper is a .NET coding add-in
58 | _ReSharper*
59 |
60 | # NCrunch
61 | *.ncrunch*
62 | .*crunch*.local.xml
63 |
64 | # Installshield output folder
65 | [Ee]xpress
66 |
67 | # DocProject is a documentation generator add-in
68 | DocProject/buildhelp/
69 | DocProject/Help/*.HxT
70 | DocProject/Help/*.HxC
71 | DocProject/Help/*.hhc
72 | DocProject/Help/*.hhk
73 | DocProject/Help/*.hhp
74 | DocProject/Help/Html2
75 | DocProject/Help/html
76 |
77 | # Click-Once directory
78 | publish
79 |
80 | # Publish Web Output
81 | *.Publish.xml
82 |
83 | # NuGet Packages Directory
84 | packages
85 |
86 | # Windows Azure Build Output
87 | csx
88 | *.build.csdef
89 |
90 | # Windows Store app package directory
91 | AppPackages/
92 |
93 | # Others
94 | [Bb]in
95 | [Oo]bj
96 | sql
97 | TestResults
98 | [Tt]est[Rr]esult*
99 | *.Cache
100 | ClientBin
101 | [Ss]tyle[Cc]op.*
102 | ~$*
103 | *.dbmdl
104 | Generated_Code #added for RIA/Silverlight projects
105 |
106 | # Backup & report files from converting an old project file to a newer
107 | # Visual Studio version. Backup files are not needed, because we have git ;-)
108 | _UpgradeReport_Files/
109 | Backup*/
110 | UpgradeLog*.XML
111 |
112 | src/Rapporteringsregisteret.Web/assets/less/*.css
113 |
114 | MetricResults/
115 | *.sln.ide/
116 |
117 | _configs/
118 |
119 | # vnext stuff
120 | wwwroot
121 | bower_components
122 | output
123 |
124 | .vs
125 |
126 |
127 | # solution specific folders
128 | ACMESharp
129 | artifacts
130 | artifacts.core
131 | artifacts64
132 | LetsEncrypt.ResourceGroup
133 | LetsEncrypt.SiteExtension.Core
134 | LetsEncrypt.SiteExtension.Test
135 | LetsEncrypt.SiteExtension.WebJob
136 | LetsEncrypt-SiteExtension
137 | media
138 | TestResults
139 | *.nupkg
140 | *.cmd
141 | *.txt
142 | settings.json
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjkp/letsencrypt-azure/dbdc477a88569f3fc63e1865c669923aacbf2d7a/.gitignore
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(MsBuildAllProjects);$(MsBuildThisFileFullPath)
4 |
5 |
6 |
7 | 1.0
8 | Let's Encrypt Azure
9 | SJKP
10 |
11 | $(VersionPrefix).$(BUILD_BUILDID)
12 |
13 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM microsoft/dotnet:2.1-sdk-alpine AS build
2 | # Set the working directory witin the container
3 | WORKDIR /src
4 |
5 | # Copy all of the source files
6 | COPY . .
7 | #COPY LetsEncrypt.Azure.DotNetCore.sln /src/LetsEncrypt.Azure.DotNetCore.sln
8 | RUN cd src && ls
9 |
10 | # Restore all packages
11 | RUN dotnet restore ./LetsEncrypt.Azure.DotNetCore.sln
12 |
13 | # Build the source code
14 | RUN dotnet build -c release ./LetsEncrypt.Azure.DotNetCore.sln
15 |
16 | RUN dotnet publish -c release ./src/LetsEncrypt.Azure.Runner/LetsEncrypt.Azure.Runner.csproj
17 |
18 |
19 | # Build runtime image
20 | FROM microsoft/dotnet:2.1-aspnetcore-runtime-alpine AS app
21 | WORKDIR /app
22 | COPY --from=build /src/src/LetsEncrypt.Azure.Runner/bin/release/netcoreapp2.1/publish .
23 |
24 | ENTRYPOINT ["dotnet", "LetsEncrypt.Azure.Runner.dll"]
--------------------------------------------------------------------------------
/LetsEncrypt.Azure.DotNetCore.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.28803.156
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt.Azure.Core.V2", "src\LetsEncrypt.Azure.Core.V2\LetsEncrypt.Azure.Core.V2.csproj", "{7D25E054-5F42-4590-A422-05719C53B981}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Letsencrypt.Azure.Core.Test", "src\LetsEncrypt.Azure.Core.Test\Letsencrypt.Azure.Core.Test.csproj", "{A15E7C6A-AA02-408D-9647-0F8C802518B3}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt.Azure.Runner", "src\LetsEncrypt.Azure.Runner\LetsEncrypt.Azure.Runner.csproj", "{6C89066E-0418-4303-809D-9F3F9BBB1013}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0752FF42-ECA0-4A8E-9CC8-2E1C2CE9B161}"
13 | ProjectSection(SolutionItems) = preProject
14 | .dockerignore = .dockerignore
15 | .gitignore = .gitignore
16 | Directory.Build.props = Directory.Build.props
17 | Dockerfile = Dockerfile
18 | readme.md = readme.md
19 | EndProjectSection
20 | EndProject
21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt.Azure.FunctionV2", "examples\LetsEncrypt.Azure.FunctionV2\LetsEncrypt.Azure.FunctionV2.csproj", "{FF8A14C9-8AC7-4057-A2EC-BA31C3965079}"
22 | EndProject
23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{33597FE6-D5E9-4F8E-9009-294BFC3D5F9A}"
24 | ProjectSection(SolutionItems) = preProject
25 | settings.json = settings.json
26 | EndProjectSection
27 | EndProject
28 | Project("{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}") = "LetsEncrypt.Azure.ResourceGroup", "src\LetsEncrypt.Azure.ResourceGroup\LetsEncrypt.Azure.ResourceGroup.deployproj", "{18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}"
29 | EndProject
30 | Global
31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
32 | Debug|Any CPU = Debug|Any CPU
33 | Debug|x64 = Debug|x64
34 | Debug|x86 = Debug|x86
35 | Release|Any CPU = Release|Any CPU
36 | Release|x64 = Release|x64
37 | Release|x86 = Release|x86
38 | EndGlobalSection
39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
40 | {7D25E054-5F42-4590-A422-05719C53B981}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {7D25E054-5F42-4590-A422-05719C53B981}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {7D25E054-5F42-4590-A422-05719C53B981}.Debug|x64.ActiveCfg = Debug|Any CPU
43 | {7D25E054-5F42-4590-A422-05719C53B981}.Debug|x64.Build.0 = Debug|Any CPU
44 | {7D25E054-5F42-4590-A422-05719C53B981}.Debug|x86.ActiveCfg = Debug|Any CPU
45 | {7D25E054-5F42-4590-A422-05719C53B981}.Debug|x86.Build.0 = Debug|Any CPU
46 | {7D25E054-5F42-4590-A422-05719C53B981}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {7D25E054-5F42-4590-A422-05719C53B981}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {7D25E054-5F42-4590-A422-05719C53B981}.Release|x64.ActiveCfg = Release|Any CPU
49 | {7D25E054-5F42-4590-A422-05719C53B981}.Release|x64.Build.0 = Release|Any CPU
50 | {7D25E054-5F42-4590-A422-05719C53B981}.Release|x86.ActiveCfg = Release|Any CPU
51 | {7D25E054-5F42-4590-A422-05719C53B981}.Release|x86.Build.0 = Release|Any CPU
52 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
54 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Debug|x64.ActiveCfg = Debug|Any CPU
55 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Debug|x64.Build.0 = Debug|Any CPU
56 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Debug|x86.ActiveCfg = Debug|Any CPU
57 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Debug|x86.Build.0 = Debug|Any CPU
58 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
59 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Release|Any CPU.Build.0 = Release|Any CPU
60 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Release|x64.ActiveCfg = Release|Any CPU
61 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Release|x64.Build.0 = Release|Any CPU
62 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Release|x86.ActiveCfg = Release|Any CPU
63 | {A15E7C6A-AA02-408D-9647-0F8C802518B3}.Release|x86.Build.0 = Release|Any CPU
64 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
65 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Debug|Any CPU.Build.0 = Debug|Any CPU
66 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Debug|x64.ActiveCfg = Debug|Any CPU
67 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Debug|x64.Build.0 = Debug|Any CPU
68 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Debug|x86.ActiveCfg = Debug|Any CPU
69 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Debug|x86.Build.0 = Debug|Any CPU
70 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Release|Any CPU.ActiveCfg = Release|Any CPU
71 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Release|Any CPU.Build.0 = Release|Any CPU
72 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Release|x64.ActiveCfg = Release|Any CPU
73 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Release|x64.Build.0 = Release|Any CPU
74 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Release|x86.ActiveCfg = Release|Any CPU
75 | {6C89066E-0418-4303-809D-9F3F9BBB1013}.Release|x86.Build.0 = Release|Any CPU
76 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
77 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Debug|Any CPU.Build.0 = Debug|Any CPU
78 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Debug|x64.ActiveCfg = Debug|Any CPU
79 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Debug|x64.Build.0 = Debug|Any CPU
80 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Debug|x86.ActiveCfg = Debug|Any CPU
81 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Debug|x86.Build.0 = Debug|Any CPU
82 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Release|Any CPU.ActiveCfg = Release|Any CPU
83 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Release|Any CPU.Build.0 = Release|Any CPU
84 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Release|x64.ActiveCfg = Release|Any CPU
85 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Release|x64.Build.0 = Release|Any CPU
86 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Release|x86.ActiveCfg = Release|Any CPU
87 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079}.Release|x86.Build.0 = Release|Any CPU
88 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
89 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
90 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Debug|x64.ActiveCfg = Debug|Any CPU
91 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Debug|x64.Build.0 = Debug|Any CPU
92 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Debug|x86.ActiveCfg = Debug|Any CPU
93 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Debug|x86.Build.0 = Debug|Any CPU
94 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
95 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Release|Any CPU.Build.0 = Release|Any CPU
96 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Release|x64.ActiveCfg = Release|Any CPU
97 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Release|x64.Build.0 = Release|Any CPU
98 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Release|x86.ActiveCfg = Release|Any CPU
99 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Release|x86.Build.0 = Release|Any CPU
100 | EndGlobalSection
101 | GlobalSection(SolutionProperties) = preSolution
102 | HideSolutionNode = FALSE
103 | EndGlobalSection
104 | GlobalSection(NestedProjects) = preSolution
105 | {FF8A14C9-8AC7-4057-A2EC-BA31C3965079} = {33597FE6-D5E9-4F8E-9009-294BFC3D5F9A}
106 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7} = {33597FE6-D5E9-4F8E-9009-294BFC3D5F9A}
107 | EndGlobalSection
108 | GlobalSection(ExtensibilityGlobals) = postSolution
109 | SolutionGuid = {5AC649FA-BB48-4484-993B-2BBCFC05742D}
110 | EndGlobalSection
111 | EndGlobal
112 |
--------------------------------------------------------------------------------
/examples/LetsEncrypt.Azure.FunctionV2/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # Azure Functions localsettings file
5 | local.settings.json
6 |
7 | # User-specific files
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | bld/
24 | [Bb]in/
25 | [Oo]bj/
26 | [Ll]og/
27 |
28 | # Visual Studio 2015 cache/options directory
29 | .vs/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # MSTest test Results
34 | [Tt]est[Rr]esult*/
35 | [Bb]uild[Ll]og.*
36 |
37 | # NUNIT
38 | *.VisualState.xml
39 | TestResult.xml
40 |
41 | # Build Results of an ATL Project
42 | [Dd]ebugPS/
43 | [Rr]eleasePS/
44 | dlldata.c
45 |
46 | # DNX
47 | project.lock.json
48 | project.fragment.lock.json
49 | artifacts/
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # NCrunch
117 | _NCrunch_*
118 | .*crunch*.local.xml
119 | nCrunchTemp_*
120 |
121 | # MightyMoose
122 | *.mm.*
123 | AutoTest.Net/
124 |
125 | # Web workbench (sass)
126 | .sass-cache/
127 |
128 | # Installshield output folder
129 | [Ee]xpress/
130 |
131 | # DocProject is a documentation generator add-in
132 | DocProject/buildhelp/
133 | DocProject/Help/*.HxT
134 | DocProject/Help/*.HxC
135 | DocProject/Help/*.hhc
136 | DocProject/Help/*.hhk
137 | DocProject/Help/*.hhp
138 | DocProject/Help/Html2
139 | DocProject/Help/html
140 |
141 | # Click-Once directory
142 | publish/
143 |
144 | # Publish Web Output
145 | *.[Pp]ublish.xml
146 | *.azurePubxml
147 | # TODO: Comment the next line if you want to checkin your web deploy settings
148 | # but database connection strings (with potential passwords) will be unencrypted
149 | #*.pubxml
150 | *.publishproj
151 |
152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
153 | # checkin your Azure Web App publish settings, but sensitive information contained
154 | # in these scripts will be unencrypted
155 | PublishScripts/
156 |
157 | # NuGet Packages
158 | *.nupkg
159 | # The packages folder can be ignored because of Package Restore
160 | **/packages/*
161 | # except build/, which is used as an MSBuild target.
162 | !**/packages/build/
163 | # Uncomment if necessary however generally it will be regenerated when needed
164 | #!**/packages/repositories.config
165 | # NuGet v3's project.json files produces more ignoreable files
166 | *.nuget.props
167 | *.nuget.targets
168 |
169 | # Microsoft Azure Build Output
170 | csx/
171 | *.build.csdef
172 |
173 | # Microsoft Azure Emulator
174 | ecf/
175 | rcf/
176 |
177 | # Windows Store app package directories and files
178 | AppPackages/
179 | BundleArtifacts/
180 | Package.StoreAssociation.xml
181 | _pkginfo.txt
182 |
183 | # Visual Studio cache files
184 | # files ending in .cache can be ignored
185 | *.[Cc]ache
186 | # but keep track of directories ending in .cache
187 | !*.[Cc]ache/
188 |
189 | # Others
190 | ClientBin/
191 | ~$*
192 | *~
193 | *.dbmdl
194 | *.dbproj.schemaview
195 | *.jfm
196 | *.pfx
197 | *.publishsettings
198 | node_modules/
199 | orleans.codegen.cs
200 |
201 | # Since there are multiple workflows, uncomment next line to ignore bower_components
202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
203 | #bower_components/
204 |
205 | # RIA/Silverlight projects
206 | Generated_Code/
207 |
208 | # Backup & report files from converting an old project file
209 | # to a newer Visual Studio version. Backup files are not needed,
210 | # because we have git ;-)
211 | _UpgradeReport_Files/
212 | Backup*/
213 | UpgradeLog*.XML
214 | UpgradeLog*.htm
215 |
216 | # SQL Server files
217 | *.mdf
218 | *.ldf
219 |
220 | # Business Intelligence projects
221 | *.rdl.data
222 | *.bim.layout
223 | *.bim_*.settings
224 |
225 | # Microsoft Fakes
226 | FakesAssemblies/
227 |
228 | # GhostDoc plugin setting file
229 | *.GhostDoc.xml
230 |
231 | # Node.js Tools for Visual Studio
232 | .ntvs_analysis.dat
233 |
234 | # Visual Studio 6 build log
235 | *.plg
236 |
237 | # Visual Studio 6 workspace options file
238 | *.opt
239 |
240 | # Visual Studio LightSwitch build output
241 | **/*.HTMLClient/GeneratedArtifacts
242 | **/*.DesktopClient/GeneratedArtifacts
243 | **/*.DesktopClient/ModelManifest.xml
244 | **/*.Server/GeneratedArtifacts
245 | **/*.Server/ModelManifest.xml
246 | _Pvt_Extensions
247 |
248 | # Paket dependency manager
249 | .paket/paket.exe
250 | paket-files/
251 |
252 | # FAKE - F# Make
253 | .fake/
254 |
255 | # JetBrains Rider
256 | .idea/
257 | *.sln.iml
258 |
259 | # CodeRush
260 | .cr/
261 |
262 | # Python Tools for Visual Studio (PTVS)
263 | __pycache__/
264 | *.pyc
--------------------------------------------------------------------------------
/examples/LetsEncrypt.Azure.FunctionV2/AutoRenewCertificate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.Azure.WebJobs;
4 | using Microsoft.Azure.WebJobs.Host;
5 | using Microsoft.Extensions.Logging;
6 |
7 | namespace LetsEncrypt.Azure.FunctionV2
8 | {
9 | public static class AutoRenewCertificate
10 | {
11 | [FunctionName("AutoRenewCertificate")]
12 | public static async Task Run([TimerTrigger("%CertRenewSchedule%", RunOnStartup = false)]TimerInfo myTimer, ILogger log)
13 | {
14 | log.LogInformation($"Renewing certificate at: {DateTime.Now}");
15 |
16 | await Helper.InstallOrRenewCertificate(log);
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/LetsEncrypt.Azure.FunctionV2/Helper.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2;
2 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
3 | using LetsEncrypt.Azure.Core.V2.Models;
4 | using Microsoft.Azure.KeyVault;
5 | using Microsoft.Azure.Services.AppAuthentication;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Logging;
9 | using System;
10 | using System.Threading.Tasks;
11 |
12 | namespace LetsEncrypt.Azure.FunctionV2
13 | {
14 | public class Helper
15 | {
16 | ///
17 | /// Requests a Let's Encrypt wild card certificate using DNS challenge.
18 | /// The DNS provider used is Azure DNS.
19 | /// The certificate is saved to Azure Key Vault.
20 | /// The Certificate is finally install to an Azure App Service.
21 | /// Configuration values are stored in Environment Variables.
22 | ///
23 | ///
24 | ///
25 | public static async Task InstallOrRenewCertificate(ILogger log)
26 | {
27 | var vaultBaseUrl = $"https://{Environment.GetEnvironmentVariable("Vault")}.vault.azure.net/";
28 | log.LogInformation("C# HTTP trigger function processed a request.");
29 | var Configuration = new ConfigurationBuilder()
30 | .AddAzureKeyVault(vaultBaseUrl) //Use MSI to get token
31 | .AddEnvironmentVariables()
32 | .Build();
33 | var tokenProvider = new AzureServiceTokenProvider();
34 | //Create the Key Vault client
35 | var kvClient = new KeyVaultClient((authority, resource, scope) => tokenProvider.KeyVaultTokenCallback(authority, resource, scope), new MessageLoggingHandler(log));
36 |
37 | IServiceCollection serviceCollection = new ServiceCollection();
38 |
39 | serviceCollection.AddSingleton(log)
40 | .Configure(options => options.MinLevel = LogLevel.Information);
41 | var certificateConsumer = Configuration.GetValue("CertificateConsumer");
42 | if (string.IsNullOrEmpty(certificateConsumer))
43 | {
44 | serviceCollection.AddAzureAppService(Configuration.GetSection("AzureAppService").Get());
45 | }
46 | else if (certificateConsumer.Equals("NullCertificateConsumer"))
47 | {
48 | serviceCollection.AddNullCertificateConsumer();
49 | }
50 |
51 | serviceCollection.AddSingleton(kvClient)
52 | .AddKeyVaultCertificateStore(vaultBaseUrl);
53 |
54 |
55 | serviceCollection.AddAcmeClient(Configuration.GetSection("DnsSettings").Get());
56 |
57 | var serviceProvider = serviceCollection.BuildServiceProvider();
58 |
59 | var app = serviceProvider.GetService();
60 |
61 | var dnsRequest = Configuration.GetSection("AcmeDnsRequest").Get();
62 |
63 | await app.Run(dnsRequest, Configuration.GetValue("RenewXNumberOfDaysBeforeExpiration") ?? 22);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/examples/LetsEncrypt.Azure.FunctionV2/LetsEncrypt.Azure.FunctionV2.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp2.1
4 | v2
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | PreserveNewest
16 |
17 |
18 | Always
19 | Never
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/LetsEncrypt.Azure.FunctionV2/RequestWildcardCertificate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Microsoft.Azure.WebJobs;
6 | using Microsoft.Azure.WebJobs.Extensions.Http;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.Extensions.Logging;
9 | using Newtonsoft.Json;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using LetsEncrypt.Azure.Core.V2;
12 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
13 | using LetsEncrypt.Azure.Core.V2.Models;
14 | using Microsoft.Extensions.Configuration;
15 | using Microsoft.Azure.Services.AppAuthentication;
16 | using Microsoft.Azure.KeyVault;
17 | using System.Web.Http;
18 |
19 | namespace LetsEncrypt.Azure.FunctionV2
20 | {
21 | public static class RequestWildcardCertificate
22 | {
23 | [FunctionName("RequestWildcardCertificate")]
24 | public static async Task Run(
25 | [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
26 | ILogger log)
27 | {
28 | try
29 | {
30 | await Helper.InstallOrRenewCertificate(log);
31 |
32 | return new OkResult();
33 | } catch(Exception ex)
34 | {
35 | log.LogError(ex.ToString());
36 | return new ExceptionResult(ex, true);
37 |
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/examples/LetsEncrypt.Azure.FunctionV2/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0"
3 | }
--------------------------------------------------------------------------------
/media/letsencrypt-azure-overiew.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjkp/letsencrypt-azure/dbdc477a88569f3fc63e1865c669923aacbf2d7a/media/letsencrypt-azure-overiew.png
--------------------------------------------------------------------------------
/media/letsencrypt-azure-overiew.vsdx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjkp/letsencrypt-azure/dbdc477a88569f3fc63e1865c669923aacbf2d7a/media/letsencrypt-azure-overiew.vsdx
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Let's Encrypt Azure
2 | [](https://dev.azure.com/letsencrypt/letsencrypt/_build/latest?definitionId=4)
3 |
4 | The easiest and most robust method for deploying Let's Encrypt Wild Card Certificate to Azure Web Apps.
5 |
6 | # Getting Started
7 | ## Azure DNS + Azure Web
8 | Deployment template for setting up Let's Encrypt wild card certificate for Azure Web App (hosting plan and web app must be colocated in same resource group). Hostname must already be configured on the Web App and the DNS must be setup in Azure.
9 |
10 |
11 |
12 | # What is Let's Encrypt Azure
13 |
14 | Let's Encrypt Azure is my second attempt to bring support for Let's Encrypt certificates on Azure. It is the spiritual successor to the Let's Encrypt Site-Extension, although they for the momemt support different usage scenarios.
15 |
16 | | Feature | Let's Encrypt Azure | Let's Encrypt Site-Extension
17 | |-----| ---- | ----
18 | | Key Vault Support | X | Not supported
19 | | Wild card SSL certificate support / DNS challenge | X | Not supported
20 | | Specific domain SSL certificate support / HTTP challenge| Planned | X
21 | | Managed Service Identity Authenticaiton | X | Not supported
22 | | Azure Web Apps | X | X
23 | | Azure CDN | Planned | Not supported
24 | | Azure Application Gateway | Planned | Not supported
25 | | Azure Front Door | Planned | Not supported
26 | | Web App behind Traffic Manager supported | X | X
27 |
28 |
29 | # How it works
30 | Let's Encrypt Azure, works by deploying a resouce group with an Azure Function that runs code that talks to Let's Encrypt to request and renew the certificate, using the DNS challenge. Since DNS challenge is used the Function app needs access to the DNS provider used for the domain. All secrets required for the process are stored in Azure Key Vault. Once a certificate is generated it can be stored a various certificate storage locations and consumed by different certificate consumers. It used application insights for storing logs.
31 |
32 | 
33 |
34 | ## Certificate Storage
35 | The recommend certificate storage location is Azure Key Vault, but is is possible to configure the Azure Function to store the certificate in Azure Blob Storage as well.
36 |
37 | ## Certificate Consumers
38 | Certificate consumers are the Azure Service that is going to consume the certificate. Right now the only supported consumer is Azure Web Apps, but more are planned for the future.
39 |
40 | * Azure Web Apps/Azure Functions
41 | * Azure Front Door (not released)
42 | * Azure Application Gateway (not released)
43 | * Azure CDN (not released)
44 |
45 | ## DNS providers
46 | DNS providers are where the DNS for the domain name is configured. Currently the following DNS providers are supported
47 |
48 | * Azure DNS
49 | * GoDaddy DNS
50 | * UnoEuro DNS
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.Test/AcmeClientTest.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core;
2 | using LetsEncrypt.Azure.Core.V2;
3 | using LetsEncrypt.Azure.Core.V2.CertificateStores;
4 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
5 | using LetsEncrypt.Azure.Core.V2.Models;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Logging;
8 | using Microsoft.VisualStudio.TestTools.UnitTesting;
9 | using System;
10 | using System.Collections.Generic;
11 | using System.IO;
12 | using System.Linq;
13 | using System.Text;
14 | using System.Threading.Tasks;
15 |
16 | namespace Letsencrypt.Azure.Core.Test
17 | {
18 | [TestClass]
19 | public class AcmeClientTest
20 | {
21 | private readonly ILogger logger;
22 |
23 | public AcmeClientTest()
24 | {
25 |
26 | }
27 |
28 | public AcmeClientTest(ILogger logger)
29 | {
30 | this.logger = logger;
31 | }
32 | [TestMethod]
33 | public async Task TestEndToEndAzure()
34 | {
35 | var config = TestHelper.AzureDnsSettings;
36 |
37 | var manager = new AcmeClient(new AzureDnsProvider(config), new DnsLookupService(), null, this.logger);
38 |
39 | var dnsRequest = new AcmeDnsRequest()
40 | {
41 | Host = "*.ai4bots.com",
42 | PFXPassword = "Pass@word",
43 | RegistrationEmail = "mail@sjkp.dk",
44 | AcmeEnvironment = new LetsEncryptStagingV2(),
45 | CsrInfo = new CsrInfo()
46 | {
47 | CountryName = "DK",
48 | Locality = "DK",
49 | Organization = "SJKP",
50 | OrganizationUnit = "",
51 | State = "DK"
52 | }
53 | };
54 |
55 | var res = await manager.RequestDnsChallengeCertificate(dnsRequest);
56 |
57 | Assert.IsNotNull(res);
58 |
59 | File.WriteAllBytes($"{dnsRequest.Host.Substring(2)}.pfx", res.CertificateInfo.PfxCertificate);
60 |
61 | var pass = new System.Security.SecureString();
62 | Array.ForEach(dnsRequest.PFXPassword.ToCharArray(), c =>
63 | {
64 | pass.AppendChar(c);
65 | });
66 | File.WriteAllBytes($"exported-{dnsRequest.Host.Substring(2)}.pfx", res.CertificateInfo.Certificate.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pkcs12, pass));
67 |
68 |
69 | var certService = new AzureWebAppService(new[] { TestHelper.AzureWebAppSettings });
70 |
71 | await certService.Install(res);
72 | }
73 |
74 | [TestMethod]
75 | public async Task TestEndToEndUnoEuro()
76 | {
77 |
78 | var dnsProvider = TestHelper.UnoEuroDnsProvider;
79 |
80 | var manager = new AcmeClient(dnsProvider, new DnsLookupService(), new NullCertificateStore());
81 |
82 | var dnsRequest = new AcmeDnsRequest()
83 | {
84 | Host = "*.tiimo.dk",
85 | PFXPassword = "Pass@word",
86 | RegistrationEmail = "mail@sjkp.dk",
87 | AcmeEnvironment = new LetsEncryptStagingV2(),
88 | CsrInfo = new CsrInfo()
89 | {
90 | CountryName = "DK",
91 | Locality = "Copenhagen",
92 | Organization = "tiimo ApS",
93 | OrganizationUnit = "",
94 | State = "DK"
95 | }
96 | };
97 |
98 | var res = await manager.RequestDnsChallengeCertificate(dnsRequest);
99 |
100 | Assert.IsNotNull(res);
101 |
102 | File.WriteAllBytes($"{dnsRequest.Host.Substring(2)}.pfx", res.CertificateInfo.PfxCertificate);
103 | }
104 |
105 |
106 | [TestMethod]
107 | public async Task TestEndToEndGoDaddy()
108 | {
109 |
110 | var dnsProvider = new GoDaddyDnsProviderTest().DnsService;
111 |
112 | var manager = new AcmeClient(dnsProvider, new DnsLookupService(), new NullCertificateStore());
113 |
114 | var dnsRequest = new AcmeDnsRequest()
115 | {
116 | Host = "*.åbningstider.info",
117 | PFXPassword = "Pass@word",
118 | RegistrationEmail = "mail@sjkp.dk",
119 | AcmeEnvironment = new LetsEncryptStagingV2(),
120 | CsrInfo = new CsrInfo()
121 | {
122 | CountryName = "DK",
123 | Locality = "Copenhagen",
124 | Organization = "Sjkp",
125 | OrganizationUnit = "",
126 | State = "DK"
127 | }
128 | };
129 |
130 | var res = await manager.RequestDnsChallengeCertificate(dnsRequest);
131 |
132 | Assert.IsNotNull(res);
133 |
134 | File.WriteAllBytes($"{dnsRequest.Host.Substring(2)}.pfx", res.CertificateInfo.PfxCertificate);
135 |
136 | var certService = new AzureWebAppService(new[] { TestHelper.AzureWebAppSettings });
137 |
138 | await certService.Install(res);
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.Test/AzureBlobStorageTest.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2;
2 | using Microsoft.Extensions.Configuration;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace Letsencrypt.Azure.Core.Test
10 | {
11 | [TestClass]
12 | public class AzureBlobStorageTest
13 | {
14 | [TestMethod]
15 | public async Task AzureBlobTest()
16 | {
17 | var config = new ConfigurationBuilder()
18 | .AddUserSecrets()
19 | .Build();
20 |
21 | var storage = new AzureBlobStorage(config["AzureStorageConnectionString"]);
22 |
23 | var filename = Guid.NewGuid().ToString();
24 |
25 | Assert.IsFalse(await storage.Exists(filename));
26 | await ValidateBinaryWrite(storage, filename, "hello world");
27 | //Assert that we can overwrite existing
28 | await ValidateBinaryWrite(storage, filename, "hello world 2");
29 |
30 | await ValidateTextWrite(storage, filename + ".txt", "text content");
31 |
32 | }
33 |
34 | private static async Task ValidateBinaryWrite(AzureBlobStorage storage, string filename, string txtcontent)
35 | {
36 | await storage.Write(filename, Encoding.UTF8.GetBytes(txtcontent));
37 |
38 | Assert.IsTrue(await storage.Exists(filename));
39 |
40 | var content = await storage.Read(filename);
41 |
42 | Assert.AreEqual(txtcontent, Encoding.UTF8.GetString(content));
43 | }
44 |
45 | private static async Task ValidateTextWrite(AzureBlobStorage storage, string filename, string txtcontent)
46 | {
47 | await storage.WriteAllText(filename, txtcontent);
48 |
49 | Assert.IsTrue(await storage.Exists(filename));
50 |
51 | var content = await storage.ReadAllText(filename);
52 |
53 | Assert.AreEqual(txtcontent, content);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.Test/AzureDnsServiceTest.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2;
2 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Rest.Azure.Authentication;
5 | using Microsoft.VisualStudio.TestTools.UnitTesting;
6 | using System;
7 | using System.Threading.Tasks;
8 |
9 | namespace Letsencrypt.Azure.Core.Test
10 | {
11 | [TestClass]
12 | public class AzureDnsServiceTest
13 | {
14 | [TestMethod]
15 | public async Task AzureDnsTest()
16 | {
17 | var config = TestHelper.AzureDnsSettings;
18 |
19 | var service = new AzureDnsProvider(config);
20 |
21 | var id = Guid.NewGuid().ToString();
22 | await service.PersistChallenge("_acme-challenge", id);
23 |
24 |
25 | var exists = await new DnsLookupService().Exists("*.ai4bots.com", id, service.MinimumTtl);
26 | Assert.IsTrue(exists);
27 |
28 | await service.Cleanup("_acme-challenge");
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.Test/GoDaddyDnsProviderTest.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2;
2 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.VisualStudio.TestTools.UnitTesting;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using static LetsEncrypt.Azure.Core.V2.DnsProviders.GoDaddyDnsProvider;
10 |
11 | namespace Letsencrypt.Azure.Core.Test
12 | {
13 | [TestClass]
14 | public class GoDaddyDnsProviderTest
15 | {
16 | private const string Domain = "åbningstider.info";
17 |
18 | public IConfiguration Configuration { get; }
19 | public GoDaddyDnsProvider DnsService { get; }
20 |
21 | public GoDaddyDnsProviderTest()
22 | {
23 | this.Configuration = new ConfigurationBuilder()
24 | .AddUserSecrets()
25 | .Build();
26 |
27 | this.DnsService = new GoDaddyDnsProvider(new GoDaddyDnsSettings()
28 | {
29 | ApiKey = this.Configuration["GoDaddyApiKey"],
30 | ApiSecret = this.Configuration["GoDaddyApiSecret"],
31 | ShopperId = this.Configuration["GoDaddyShopperId"],
32 | Domain = Domain
33 | });
34 | }
35 |
36 | [TestMethod]
37 | public async Task TestPersistChallenge()
38 | {
39 | var id = Guid.NewGuid().ToString();
40 | await DnsService.PersistChallenge("_acme-challenge", id);
41 |
42 |
43 | var exists = await new DnsLookupService().Exists("*." + Domain, id);
44 | Assert.IsTrue(exists);
45 |
46 | await DnsService.Cleanup("_acme-challenge");
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.Test/Letsencrypt.Azure.Core.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.1
5 |
6 | false
7 | 5F9376DE-25E6-4FFF-8462-D95812FCE06C
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Always
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.Test/LoggingTest.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.VisualStudio.TestTools.UnitTesting;
4 | using System.Threading.Tasks;
5 |
6 | namespace Letsencrypt.Azure.Core.Test
7 | {
8 | [TestClass]
9 | public class LoggingTest
10 | {
11 | [TestMethod]
12 | public async Task TestLogging()
13 | {
14 | ILoggerFactory loggerFactory = new LoggerFactory()
15 | .AddConsole()
16 | .AddDebug();
17 | var logger = loggerFactory.CreateLogger();
18 | logger.LogInformation("Initial message");
19 |
20 | var client = new AcmeClientTest(logger);
21 | await client.TestEndToEndAzure();
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.Test/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Letsencrypt.Azure.Core.Test": {
4 | "commandName": "Project"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.Test/TestHelper.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
2 | using LetsEncrypt.Azure.Core.V2.Models;
3 | using Microsoft.Azure.Management.Dns.Fluent;
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Rest.Azure.Authentication;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 |
11 | namespace Letsencrypt.Azure.Core.Test
12 | {
13 | public class TestHelper
14 | {
15 | private static readonly string tenantId;
16 | private static readonly string subscriptionId;
17 | private static string clientId;
18 | private static string secret;
19 |
20 | static TestHelper()
21 | {
22 | var config = new ConfigurationBuilder()
23 | .AddUserSecrets()
24 | .Build();
25 |
26 | tenantId = config["tenantId"];
27 | subscriptionId = config["subscriptionId"];
28 | clientId = config["clientId"];
29 | secret = config["clientSecret"];
30 | }
31 | public static AzureDnsSettings AzureDnsSettings
32 | {
33 | get
34 | {
35 | return new AzureDnsSettings("dns", "ai4bots.com", AzureServicePrincipal, new AzureSubscription()
36 | {
37 | AzureRegion = "AzureGlobalCloud",
38 | SubscriptionId = subscriptionId,
39 | Tenant = tenantId
40 | });
41 | }
42 | }
43 |
44 | public static UnoEuroDnsProvider UnoEuroDnsProvider
45 | {
46 | get
47 | {
48 | var config = new ConfigurationBuilder()
49 | .AddUserSecrets()
50 | .Build();
51 |
52 | return new UnoEuroDnsProvider(new UnoEuroDnsSettings()
53 | {
54 | AccountName = config["accountName"],
55 | ApiKey = config["apiKey"],
56 | Domain = config["domain"]
57 | });
58 | }
59 | }
60 |
61 | public static AzureServicePrincipal AzureServicePrincipal => new AzureServicePrincipal()
62 | {
63 | ClientId = clientId,
64 | ClientSecret = secret
65 | };
66 |
67 | public static AzureWebAppSettings AzureWebAppSettings
68 | {
69 | get
70 | {
71 | return new AzureWebAppSettings("webappcfmv5fy7lcq7o", "LetsEncrypt-SiteExtension2", AzureServicePrincipal, new AzureSubscription()
72 | {
73 | Tenant = tenantId,
74 | SubscriptionId = "3f09c367-93e0-4b61-bbe5-dcb5c686bf8a",
75 | AzureRegion = "AzureGlobalCloud"
76 | });
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.Test/UnoEuroDnsProviderTest.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2;
2 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.VisualStudio.TestTools.UnitTesting;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace Letsencrypt.Azure.Core.Test
11 | {
12 | [TestClass]
13 | public class UnoEuroDnsProviderTest
14 | {
15 | [TestMethod]
16 | public async Task CreateRecord()
17 | {
18 | var config = new ConfigurationBuilder()
19 | .AddUserSecrets()
20 | .Build();
21 |
22 | var dnsProvider = new UnoEuroDnsProvider(new UnoEuroDnsSettings()
23 | {
24 | AccountName = config["accountName"],
25 | ApiKey = config["apiKey"],
26 | Domain = config["domain"]
27 | });
28 | //Test create new
29 | await dnsProvider.PersistChallenge("_acme-challenge", Guid.NewGuid().ToString());
30 | //Test Update existing
31 | await dnsProvider.PersistChallenge("_acme-challenge", Guid.NewGuid().ToString());
32 | //Test clean up
33 | await dnsProvider.Cleanup("_acme-challenge");
34 |
35 | }
36 |
37 | [TestMethod]
38 | public async Task UnoEuroDnsTest()
39 | {
40 | var service = TestHelper.UnoEuroDnsProvider;
41 |
42 | var id = Guid.NewGuid().ToString();
43 | await service.PersistChallenge("_acme-challenge", id);
44 |
45 |
46 | var exists = await new DnsLookupService().Exists("*.tiimo.dk", id, service.MinimumTtl);
47 | Assert.IsTrue(exists);
48 |
49 | await service.Cleanup("_acme-challenge");
50 | }
51 |
52 |
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/AcmeClient.cs:
--------------------------------------------------------------------------------
1 | using Certes;
2 | using Certes.Acme;
3 | using Certes.Acme.Resource;
4 | using LetsEncrypt.Azure.Core.V2.CertificateStores;
5 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
6 | using LetsEncrypt.Azure.Core.V2.Models;
7 | using Microsoft.Extensions.Logging;
8 | using Microsoft.Extensions.Logging.Abstractions;
9 | using System;
10 | using System.Globalization;
11 | using System.Linq;
12 | using System.Net.Http;
13 | using System.Security.Cryptography.X509Certificates;
14 | using System.Threading.Tasks;
15 |
16 | namespace LetsEncrypt.Azure.Core.V2
17 | {
18 | public class AcmeClient
19 | {
20 | private readonly IDnsProvider dnsProvider;
21 | private readonly DnsLookupService dnsLookupService;
22 | private readonly ICertificateStore certificateStore;
23 |
24 | private readonly ILogger logger;
25 |
26 | public AcmeClient(IDnsProvider dnsProvider, DnsLookupService dnsLookupService, ICertificateStore certifcateStore, ILogger logger = null)
27 | {
28 | this.dnsProvider = dnsProvider;
29 | this.dnsLookupService = dnsLookupService;
30 | this.certificateStore = certifcateStore;
31 | this.logger = logger ?? NullLogger.Instance;
32 |
33 | }
34 | ///
35 | /// Request a certificate from lets encrypt using the DNS challenge, placing the challenge record in Azure DNS.
36 | /// The certifiacte is not assigned, but just returned.
37 | ///
38 | ///
39 | ///
40 | ///
41 | public async Task RequestDnsChallengeCertificate(IAcmeDnsRequest acmeConfig)
42 | {
43 | logger.LogInformation("Starting request DNS Challenge certificate for {AcmeEnvironment} and {Email}", acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.RegistrationEmail);
44 | var acmeContext = await GetOrCreateAcmeContext(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.RegistrationEmail);
45 | var idn = new IdnMapping();
46 |
47 | var order = await acmeContext.NewOrder(new[] { "*." + idn.GetAscii(acmeConfig.Host.Substring(2)) });
48 | var a = await order.Authorizations();
49 | var authz = a.First();
50 | var challenge = await authz.Dns();
51 | var dnsTxt = acmeContext.AccountKey.DnsTxt(challenge.Token);
52 | logger.LogInformation("Got DNS challenge token {Token}", dnsTxt);
53 |
54 | ///add dns entry
55 | await this.dnsProvider.PersistChallenge("_acme-challenge", dnsTxt);
56 |
57 | if (!(await this.dnsLookupService.Exists(acmeConfig.Host, dnsTxt, this.dnsProvider.MinimumTtl)))
58 | {
59 | throw new TimeoutException($"Unable to validate that _acme-challenge was stored in txt _acme-challenge record after {this.dnsProvider.MinimumTtl} seconds");
60 | }
61 |
62 |
63 | Challenge chalResp = await challenge.Validate();
64 | while (chalResp.Status == ChallengeStatus.Pending || chalResp.Status == ChallengeStatus.Processing)
65 | {
66 | logger.LogInformation("Dns challenge response status {ChallengeStatus} more info at {ChallengeStatusUrl} retrying in 5 sec", chalResp.Status, chalResp.Url.ToString());
67 | await Task.Delay(5000);
68 | chalResp = await challenge.Resource();
69 | }
70 |
71 | logger.LogInformation("Finished validating dns challenge token, response was {ChallengeStatus} more info at {ChallengeStatusUrl}", chalResp.Status, chalResp.Url);
72 |
73 | var privateKey = await GetOrCreateKey(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.Host);
74 | var cert = await order.Generate(new Certes.CsrInfo
75 | {
76 | CountryName = acmeConfig.CsrInfo?.CountryName,
77 | State = acmeConfig.CsrInfo?.State,
78 | Locality = acmeConfig.CsrInfo?.Locality,
79 | Organization = acmeConfig.CsrInfo?.Organization,
80 | OrganizationUnit = acmeConfig.CsrInfo?.OrganizationUnit
81 | }, privateKey);
82 |
83 | var certPem = cert.ToPem();
84 |
85 | var pfxBuilder = cert.ToPfx(privateKey);
86 | var pfx = pfxBuilder.Build(acmeConfig.Host, acmeConfig.PFXPassword);
87 |
88 | await this.dnsProvider.Cleanup(dnsTxt);
89 |
90 | return new CertificateInstallModel()
91 | {
92 | CertificateInfo = new CertificateInfo()
93 | {
94 | Certificate = new X509Certificate2(pfx, acmeConfig.PFXPassword, X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable),
95 | Name = $"{acmeConfig.Host} {DateTime.Now}",
96 | Password = acmeConfig.PFXPassword,
97 | PfxCertificate = pfx
98 | },
99 | Host = acmeConfig.Host
100 | };
101 | }
102 |
103 | private async Task GetOrCreateKey(Uri acmeDirectory, string host)
104 | {
105 | string secretName = $"privatekey{host}--{acmeDirectory.Host}";
106 | var key = await this.certificateStore.GetSecret(secretName);
107 | if (string.IsNullOrEmpty(key))
108 | {
109 | var privatekey = KeyFactory.NewKey(KeyAlgorithm.RS256);
110 | await this.certificateStore.SaveSecret(secretName, privatekey.ToPem());
111 | return privatekey;
112 | }
113 |
114 | return KeyFactory.FromPem(key);
115 | }
116 |
117 | private async Task GetOrCreateAcmeContext(Uri acmeDirectoryUri, string email)
118 | {
119 | AcmeContext acme = null;
120 | string filename = $"account{email}--{acmeDirectoryUri.Host}";
121 | var secret = await this.certificateStore.GetSecret(filename);
122 | if (string.IsNullOrEmpty(secret))
123 | {
124 | acme = new AcmeContext(acmeDirectoryUri);
125 | var account = acme.NewAccount(email, true);
126 |
127 | // Save the account key for later use
128 | var pemKey = acme.AccountKey.ToPem();
129 | await certificateStore.SaveSecret(filename, pemKey);
130 | // await Task.Delay(10000); //Wait a little before using the new account.
131 | acme = new AcmeContext(acmeDirectoryUri, acme.AccountKey, new AcmeHttpClient(acmeDirectoryUri, new HttpClient(new MessageLoggingHandler(this.logger))));
132 | }
133 | else
134 | {
135 | var accountKey = KeyFactory.FromPem(secret);
136 | acme = new AcmeContext(acmeDirectoryUri, accountKey, new AcmeHttpClient(acmeDirectoryUri, new HttpClient(new MessageLoggingHandler(this.logger))));
137 | }
138 |
139 | return acme;
140 | }
141 |
142 |
143 |
144 |
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/AzureBlobStorage.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.WindowsAzure.Storage;
2 | using Microsoft.WindowsAzure.Storage.Blob;
3 | using System.IO;
4 | using System.Threading.Tasks;
5 |
6 | namespace LetsEncrypt.Azure.Core.V2
7 | {
8 | public class AzureBlobStorage : IFileSystem
9 | {
10 | private CloudStorageAccount storageAccount;
11 |
12 | public AzureBlobStorage(string connectionString)
13 | {
14 | this.storageAccount = CloudStorageAccount.Parse(connectionString);
15 | }
16 |
17 | public async Task Exists(string v)
18 | {
19 | CloudBlockBlob blob = await GetBlob(v);
20 | return await blob.ExistsAsync();
21 | }
22 |
23 | private async Task GetBlob(string v)
24 | {
25 | var client = storageAccount.CreateCloudBlobClient();
26 | var container = client.GetContainerReference("letsencrypt");
27 | await container.CreateIfNotExistsAsync();
28 |
29 |
30 | var blob = container.GetBlockBlobReference(v);
31 |
32 | return blob;
33 | }
34 |
35 | public async Task ReadAllText(string v)
36 | {
37 | var blob = await GetBlob(v);
38 | return await blob.DownloadTextAsync();
39 | }
40 |
41 | public async Task WriteAllText(string v, string pemKey)
42 | {
43 | var blob = await GetBlob(v);
44 | await blob.UploadTextAsync(pemKey);
45 | }
46 |
47 | public async Task Read(string v)
48 | {
49 | var blob = await GetBlob(v);
50 | using (var ms = new MemoryStream())
51 | using (var data = await blob.OpenReadAsync())
52 | {
53 | await data.CopyToAsync(ms);
54 | return ms.ToArray();
55 | }
56 | }
57 |
58 | public async Task Write(string v, byte[] data)
59 | {
60 | var blob = await GetBlob(v);
61 | using (var ms = new MemoryStream(data))
62 | await blob.UploadFromStreamAsync(ms);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/AzureHelper.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2.Models;
2 | using Microsoft.Azure.Management.ResourceManager.Fluent.Authentication;
3 | using Microsoft.Azure.Management.ResourceManager.Fluent.Core;
4 | using Microsoft.Azure.Services.AppAuthentication;
5 | using Microsoft.Rest;
6 | using System;
7 |
8 | namespace LetsEncrypt.Azure.Core.V2
9 | {
10 | public class AzureHelper
11 | {
12 | public static AzureCredentials GetAzureCredentials(AzureServicePrincipal servicePrincipal, AzureSubscription azureSubscription)
13 | {
14 | if (servicePrincipal == null)
15 | {
16 | throw new ArgumentNullException(nameof(servicePrincipal));
17 | }
18 |
19 | if (azureSubscription == null)
20 | {
21 | throw new ArgumentNullException(nameof(azureSubscription));
22 | }
23 |
24 | if (servicePrincipal.UseManagendIdentity)
25 | {
26 | return new AzureCredentials(new MSILoginInformation(MSIResourceType.AppService), Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment.FromName(azureSubscription.AzureRegion));
27 | }
28 |
29 |
30 | return new AzureCredentials(servicePrincipal.ServicePrincipalLoginInformation,
31 | azureSubscription.Tenant, Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment.FromName(azureSubscription.AzureRegion));
32 | }
33 |
34 |
35 | public static RestClient GetRestClient(AzureServicePrincipal servicePrincipal, AzureSubscription azureSubscription)
36 | {
37 | var credentials = GetAzureCredentials(servicePrincipal, azureSubscription);
38 | return RestClient
39 | .Configure()
40 | .WithEnvironment(Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment.FromName(azureSubscription.AzureRegion))
41 | .WithCredentials(credentials)
42 | .Build();
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/CertificateConsumers/AzureWebAppService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.Azure.Management.AppService.Fluent;
6 | using Microsoft.Azure.Management.AppService.Fluent.Models;
7 | using LetsEncrypt.Azure.Core.V2.Models;
8 | using Microsoft.Extensions.Logging;
9 | using Microsoft.Extensions.Logging.Abstractions;
10 | using LetsEncrypt.Azure.Core.V2.CertificateConsumers;
11 |
12 | namespace LetsEncrypt.Azure.Core.V2
13 | {
14 | public class AzureWebAppService : ICertificateConsumer
15 | {
16 | private readonly AzureWebAppSettings[] settings;
17 | private readonly ILogger logger;
18 |
19 | public AzureWebAppService(AzureWebAppSettings[] settings, ILogger logger = null)
20 | {
21 | this.settings = settings;
22 | this.logger = logger ?? NullLogger.Instance;
23 | }
24 | public async Task Install(ICertificateInstallModel model)
25 | {
26 | logger.LogInformation("Starting installation of certificate {Thumbprint} for {Host}", model.CertificateInfo.Certificate.Thumbprint, model.Host);
27 | var cert = model.CertificateInfo;
28 | foreach (var setting in this.settings)
29 | {
30 | logger.LogInformation("Installing certificate for web app {WebApp}", setting.WebAppName);
31 | try
32 | {
33 | IAppServiceManager appServiceManager = GetAppServiceManager(setting);
34 | var s = appServiceManager.WebApps.GetByResourceGroup(setting.ResourceGroupName, setting.WebAppName);
35 | IWebAppBase siteOrSlot = s;
36 | if (!string.IsNullOrEmpty(setting.SiteSlotName))
37 | {
38 | var slot = s.DeploymentSlots.GetByName(setting.SiteSlotName);
39 | siteOrSlot = slot;
40 | }
41 |
42 | var existingCerts = await appServiceManager.AppServiceCertificates.ListByResourceGroupAsync(setting.ServicePlanOrResourceGroupName);
43 | if (existingCerts.All(_ => _.Thumbprint != cert.Certificate.Thumbprint))
44 | {
45 | await appServiceManager.AppServiceCertificates.Define(model.Host + "-" + cert.Certificate.Thumbprint).WithRegion(s.RegionName).WithExistingResourceGroup(setting.ServicePlanOrResourceGroupName).WithPfxByteArray(model.CertificateInfo.PfxCertificate).WithPfxPassword(model.CertificateInfo.Password).CreateAsync();
46 | }
47 |
48 |
49 |
50 | var sslStates = siteOrSlot.HostNameSslStates;
51 | var domainSslMappings = new List>(sslStates.Where(_ => _.Key.Contains($".{model.Host.Substring(2)}")));
52 |
53 | if (domainSslMappings.Any())
54 | {
55 | foreach (var domainMapping in domainSslMappings)
56 | {
57 |
58 | string hostName = domainMapping.Value.Name;
59 | if (domainMapping.Value.Thumbprint == cert.Certificate.Thumbprint)
60 | continue;
61 | logger.LogInformation("Binding certificate {Thumbprint} to {Host}", model.CertificateInfo.Certificate.Thumbprint, hostName);
62 | var binding = new HostNameBindingInner()
63 | {
64 | SslState = setting.UseIPBasedSSL ? SslState.IpBasedEnabled : SslState.SniEnabled,
65 | Thumbprint = model.CertificateInfo.Certificate.Thumbprint
66 | };
67 | if (!string.IsNullOrEmpty(setting.SiteSlotName))
68 | {
69 | await appServiceManager.Inner.WebApps.CreateOrUpdateHostNameBindingSlotAsync(setting.ServicePlanOrResourceGroupName, setting.WebAppName, hostName, binding, setting.SiteSlotName);
70 | }
71 | else
72 | {
73 | await appServiceManager.Inner.WebApps.CreateOrUpdateHostNameBindingAsync(setting.ServicePlanOrResourceGroupName, setting.WebAppName, hostName, binding);
74 | }
75 | }
76 | }
77 | }
78 | catch (Exception e)
79 | {
80 | logger.LogCritical(e, "Unable to install certificate for '{WebApp}'", setting.WebAppName);
81 | throw;
82 | }
83 | }
84 | }
85 |
86 | private static IAppServiceManager GetAppServiceManager(AzureWebAppSettings settings)
87 | {
88 | var restClient = AzureHelper.GetRestClient(settings.AzureServicePrincipal, settings.AzureSubscription);
89 | return new AppServiceManager(restClient, settings.AzureSubscription.SubscriptionId, settings.AzureSubscription.Tenant);
90 | }
91 |
92 | public async Task> CleanUp()
93 | {
94 | return await this.CleanUp(0);
95 | }
96 | public async Task> CleanUp(int removeXNumberOfDaysBeforeExpiration = 0)
97 | {
98 | var removedCerts = new List();
99 | foreach (var setting in this.settings)
100 | {
101 | var appServiceManager = GetAppServiceManager(setting);
102 | var certs = await appServiceManager.AppServiceCertificates.ListByResourceGroupAsync(setting.ServicePlanOrResourceGroupName);
103 |
104 | var tobeRemoved = certs.Where(s => s.ExpirationDate < DateTime.UtcNow.AddDays(removeXNumberOfDaysBeforeExpiration) && (s.Issuer.Contains("Let's Encrypt") || s.Issuer.Contains("Fake LE"))).ToList();
105 |
106 | tobeRemoved.ForEach(async s => await RemoveCertificate(appServiceManager, s, setting));
107 |
108 | removedCerts.AddRange(tobeRemoved.Select(s => s.Thumbprint).ToList());
109 | }
110 | return removedCerts;
111 | }
112 |
113 | private async Task RemoveCertificate(IAppServiceManager webSiteClient, IAppServiceCertificate s, AzureWebAppSettings setting)
114 | {
115 | await webSiteClient.AppServiceCertificates.DeleteByResourceGroupAsync(setting.ServicePlanOrResourceGroupName, s.Name);
116 | }
117 |
118 |
119 | }
120 | }
121 |
122 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/CertificateConsumers/ICertificateConsumer.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2.Models;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace LetsEncrypt.Azure.Core.V2.CertificateConsumers
8 | {
9 | public interface ICertificateConsumer
10 | {
11 | ///
12 | /// Installs/assigns the new certificate.
13 | ///
14 | ///
15 | ///
16 | Task Install(ICertificateInstallModel model);
17 |
18 | ///
19 | /// Remove any expired certificates
20 | ///
21 | /// List of thumbprint for certificates removed
22 | Task> CleanUp();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/CertificateConsumers/NullCertificateConsumer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using LetsEncrypt.Azure.Core.V2.Models;
7 |
8 | namespace LetsEncrypt.Azure.Core.V2.CertificateConsumers
9 | {
10 | ///
11 | /// Certificate consumer that does do anything.
12 | ///
13 | public class NullCertificateConsumer : ICertificateConsumer
14 | {
15 | public Task> CleanUp()
16 | {
17 | return Task.FromResult(new List());
18 | }
19 |
20 | public Task Install(ICertificateInstallModel model)
21 | {
22 | return Task.CompletedTask;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/CertificateStores/AzureBlobCertificateStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace LetsEncrypt.Azure.Core.V2.CertificateStores
6 | {
7 | public class AzureBlobCertificateStore : FileSystemBase
8 | {
9 | public AzureBlobCertificateStore(AzureBlobStorage azureBlobStorage) : base(azureBlobStorage)
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/CertificateStores/AzureKeyVaultCertificateStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Cryptography.X509Certificates;
3 | using System.Text.RegularExpressions;
4 | using System.Threading.Tasks;
5 | using LetsEncrypt.Azure.Core.V2.Models;
6 | using Microsoft.Azure.KeyVault;
7 | using Microsoft.Azure.KeyVault.Models;
8 |
9 | namespace LetsEncrypt.Azure.Core.V2.CertificateStores
10 | {
11 | /// An azure key vault certificate store.
12 | ///
13 | public class AzureKeyVaultCertificateStore : ICertificateStore
14 | {
15 | /// The key vault client.
16 | private readonly IKeyVaultClient keyVaultClient;
17 |
18 | /// The base URL for the key vault, typically https://{keyvaultname}.vault.azure.net
19 | public string vaultBaseUrl { get; }
20 |
21 | ///
22 | /// Initializes a new instance of the class.
23 | ///
24 | /// The key vault client.
25 | ///
26 | /// The base URL for the key vault, typically https://{keyvaultname}.vault.azure.net.
27 | ///
28 | public AzureKeyVaultCertificateStore(IKeyVaultClient keyVaultClient, string vaultBaseUrl)
29 | {
30 | this.keyVaultClient = keyVaultClient;
31 | this.vaultBaseUrl = vaultBaseUrl;
32 | }
33 |
34 | public async Task GetCertificate(string name, string password)
35 | {
36 | // This retrieves the certificate with private key (without password), see https://blogs.technet.microsoft.com/kv/2016/09/26/get-started-with-azure-key-vault-certificates/
37 | var secretName = CleanName(name);
38 | var cer = await this.GetSecret(secretName);
39 | if (cer == null)
40 | {
41 | return null;
42 | }
43 |
44 |
45 | var cert = new X509Certificate2(Convert.FromBase64String(cer), default(string), X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
46 |
47 |
48 |
49 | return new CertificateInfo()
50 | {
51 | Certificate = cert,
52 | Name = name,
53 | Password = password,
54 | PfxCertificate = cert.Export(X509ContentType.Pfx, password)
55 | };
56 | }
57 |
58 | /// Saves a certificate.
59 | /// The certificate.
60 | /// An asynchronous result.
61 | public Task SaveCertificate(CertificateInfo certificate)
62 | {
63 | return this.keyVaultClient.ImportCertificateAsync(this.vaultBaseUrl, CleanName(certificate.Name), Convert.ToBase64String(certificate.PfxCertificate), certificate.Password);
64 | }
65 |
66 | private string CleanName(string name)
67 | {
68 | Regex regex = new Regex("[^a-zA-Z0-9-]");
69 | return regex.Replace(name, "");
70 | }
71 |
72 | public async Task GetSecret(string name)
73 | {
74 | var secretName = CleanName(name);
75 | // This retrieves the secret/certificate with the private key
76 | SecretBundle secret = null;
77 | try
78 | {
79 | secret = await this.keyVaultClient.GetSecretAsync(this.vaultBaseUrl, secretName);
80 | }
81 | catch (KeyVaultErrorException kvex)
82 | {
83 | if (kvex.Body.Error.Code == "SecretNotFound")
84 | {
85 | return null;
86 | }
87 | throw;
88 | }
89 | return secret.Value;
90 | }
91 |
92 | public Task SaveSecret(string name, string secret)
93 | {
94 | return this.keyVaultClient.SetSecretAsync(this.vaultBaseUrl, CleanName(name), secret);
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/CertificateStores/FileSystemBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Security.Cryptography.X509Certificates;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using LetsEncrypt.Azure.Core.V2.Models;
7 |
8 | namespace LetsEncrypt.Azure.Core.V2.CertificateStores
9 | {
10 | public abstract class FileSystemBase : ICertificateStore
11 | {
12 | private readonly IFileSystem fileSystem;
13 | private const string fileExtension = ".pfx";
14 |
15 | public FileSystemBase(IFileSystem fileSystem)
16 | {
17 | this.fileSystem = fileSystem;
18 |
19 | }
20 |
21 | public async Task GetCertificate(string name, string password)
22 | {
23 | var filename = name + fileExtension;
24 | if (! await this.fileSystem.Exists(filename))
25 | return null;
26 | var pfx = await this.fileSystem.Read(filename);
27 | return new CertificateInfo()
28 | {
29 | PfxCertificate = pfx,
30 | Certificate = new X509Certificate2(pfx, password, X509KeyStorageFlags.DefaultKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable),
31 | Name = name,
32 | Password = password
33 | };
34 | }
35 |
36 | public async Task SaveCertificate(CertificateInfo certificate)
37 | {
38 | await this.fileSystem.Write(certificate.Name+fileExtension, certificate.PfxCertificate);
39 | }
40 |
41 | public async Task GetSecret(string name)
42 | {
43 | var filename = name + fileExtension;
44 | if (!await this.fileSystem.Exists(filename))
45 | return null;
46 | return System.Text.Encoding.UTF8.GetString(await this.fileSystem.Read(filename));
47 | }
48 |
49 | public async Task SaveSecret(string name, string secret)
50 | {
51 | await this.fileSystem.Write(name + fileExtension, Encoding.UTF8.GetBytes(secret));
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/CertificateStores/FileSystemCertificateStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace LetsEncrypt.Azure.Core.V2.CertificateStores
6 | {
7 | public class FileSystemCertificateStore : FileSystemBase
8 | {
9 | public FileSystemCertificateStore() : base(new FileSystem())
10 | {
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/CertificateStores/ICertificateStore.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2.Models;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace LetsEncrypt.Azure.Core.V2.CertificateStores
8 | {
9 | public interface ICertificateStore
10 | {
11 | Task GetSecret(string name);
12 | Task SaveSecret(string name, string secret);
13 |
14 | Task GetCertificate(string name, string password);
15 | Task SaveCertificate(CertificateInfo certificate);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/CertificateStores/NullCertificateStore.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using LetsEncrypt.Azure.Core.V2.Models;
3 |
4 | namespace LetsEncrypt.Azure.Core.V2.CertificateStores
5 | {
6 | public class NullCertificateStore : ICertificateStore
7 | {
8 | public Task GetCertificate(string name, string password)
9 | {
10 | return Task.FromResult(null);
11 | }
12 |
13 | public Task GetSecret(string name)
14 | {
15 | return Task.FromResult(null);
16 | }
17 |
18 | public Task SaveCertificate(CertificateInfo certificate)
19 | {
20 | return Task.CompletedTask;
21 | }
22 |
23 | public Task SaveSecret(string name, string secret)
24 | {
25 | return Task.CompletedTask;
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/DnsLookupService.cs:
--------------------------------------------------------------------------------
1 | using DnsClient;
2 | using Microsoft.Extensions.Logging;
3 | using Microsoft.Extensions.Logging.Abstractions;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Globalization;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 |
11 | namespace LetsEncrypt.Azure.Core.V2
12 | {
13 | public class DnsLookupService
14 | {
15 | private readonly ILogger logger;
16 |
17 | public DnsLookupService(ILogger logger = null)
18 | {
19 | this.logger = logger ?? NullLogger.Instance;
20 | }
21 |
22 | public async Task Exists(string hostname, string dnsTxt, int timeout = 60)
23 | {
24 | logger.LogInformation("Starting dns precheck validation for hostname: {HostName} challenge: {Challenge} and timeout {Timeout}", hostname, dnsTxt, timeout);
25 | var idn = new IdnMapping();
26 | hostname = idn.GetAscii(GetNoneWildcardDomain(hostname));
27 | var dnsClient = GetDnsClient(hostname);
28 | var startTime = DateTime.UtcNow;
29 | string queriedDns = "";
30 | //Lets encrypt checks a random authoritative server, thus we need to ensure that all respond with the challenge.
31 | foreach (var ns in dnsClient.NameServers)
32 | {
33 | logger.LogInformation("Validating dns challenge exists on name server {NameServer}", ns.ToString());
34 | do
35 | {
36 | var dnsRes = dnsClient.QueryServer(new[] { ns.Endpoint.Address }, $"_acme-challenge.{hostname}", QueryType.TXT);
37 | queriedDns = dnsRes.Answers.TxtRecords().FirstOrDefault()?.Text.FirstOrDefault();
38 | if (queriedDns != dnsTxt)
39 | {
40 | logger.LogInformation("Challenge record was {existingTxt} should have been {Challenge}, retrying again in 5 seconds", queriedDns, dnsTxt);
41 | await Task.Delay(5000);
42 | }
43 |
44 | } while (queriedDns != dnsTxt && (DateTime.UtcNow - startTime).TotalSeconds < timeout);
45 | }
46 |
47 | return queriedDns == dnsTxt;
48 | }
49 |
50 | private static LookupClient GetDnsClient(params string[] hostnames)
51 | {
52 |
53 | LookupClient generalClient = new LookupClient();
54 | LookupClient dnsClient = null;
55 | generalClient.UseCache = false;
56 | foreach (var hostname in hostnames)
57 | {
58 | var ns = generalClient.Query(hostname, QueryType.NS);
59 | var ip = ns.Answers.NsRecords().Select(s => generalClient.GetHostEntry(s.NSDName.Value));
60 |
61 | dnsClient = new LookupClient(ip.SelectMany(i => i.AddressList).Where(s => s.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork).ToArray());
62 | dnsClient.UseCache = false;
63 |
64 | }
65 |
66 | return dnsClient;
67 | }
68 |
69 | public static string GetNoneWildcardDomain(string hostname)
70 | {
71 | return hostname.Replace("*.", "");
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/DnsProviders/AzureDnsProvider.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2.Models;
2 | using Microsoft.Azure.Management.Dns.Fluent;
3 | using Microsoft.Azure.Management.Dns.Fluent.Models;
4 | using Microsoft.Azure.Management.ResourceManager.Fluent;
5 | using Microsoft.Azure.Management.ResourceManager.Fluent.Core;
6 | using Microsoft.Rest.Azure;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Threading.Tasks;
10 |
11 | namespace LetsEncrypt.Azure.Core.V2.DnsProviders
12 | {
13 | public class AzureDnsProvider : IDnsProvider
14 | {
15 | private readonly IDnsManagementClient client;
16 | private readonly AzureDnsSettings settings;
17 |
18 | public AzureDnsProvider(AzureDnsSettings settings)
19 | {
20 | var restClient = AzureHelper.GetRestClient(settings.AzureServicePrincipal, settings.AzureSubscription);
21 |
22 |
23 | this.client = new DnsManagementClient(restClient);
24 | this.client.SubscriptionId = settings.AzureSubscription.SubscriptionId;
25 | this.settings = settings;
26 | }
27 |
28 | public int MinimumTtl => 60;
29 |
30 | public async Task Cleanup(string recordSetName)
31 | {
32 | var existingRecords = await SafeGetExistingRecords(recordSetName);
33 |
34 | await this.client.RecordSets.DeleteAsync(this.settings.ResourceGroupName, this.settings.ZoneName, GetRelativeRecordSetName(recordSetName), RecordType.TXT);
35 | }
36 |
37 | public async Task PersistChallenge(string recordSetName, string recordValue)
38 | {
39 | List records = new List()
40 | {
41 | new TxtRecord() { Value = new[] { recordValue } }
42 | };
43 | if ((await client.RecordSets.ListByTypeAsync(settings.ResourceGroupName, settings.ZoneName, RecordType.TXT)).Any())
44 | {
45 | var existingRecords = await SafeGetExistingRecords(recordSetName);
46 | if (existingRecords != null)
47 | {
48 | if (existingRecords.TxtRecords.Any(s => s.Value.Contains(recordValue)))
49 | {
50 | records = existingRecords.TxtRecords.ToList();
51 | }
52 | else
53 | {
54 | records.AddRange(existingRecords.TxtRecords);
55 | }
56 | }
57 | }
58 | await this.client.RecordSets.CreateOrUpdateAsync(this.settings.ResourceGroupName, this.settings.ZoneName, GetRelativeRecordSetName(recordSetName), RecordType.TXT, new RecordSetInner()
59 | {
60 | TxtRecords = records,
61 | TTL = MinimumTtl
62 | });
63 | }
64 |
65 | private string GetRelativeRecordSetName(string dnsTxt)
66 | {
67 | return dnsTxt.Replace($".{this.settings.ZoneName}", "");
68 | }
69 |
70 | private async Task SafeGetExistingRecords(string recordSetName)
71 | {
72 | try
73 | {
74 | return await client.RecordSets.GetAsync(settings.ResourceGroupName, settings.ZoneName, GetRelativeRecordSetName(recordSetName), RecordType.TXT);
75 |
76 | }
77 | catch (CloudException cex)
78 | {
79 | if (!cex.Message.StartsWith("The resource record "))
80 | {
81 | throw;
82 | }
83 | }
84 | return null;
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/DnsProviders/GoDaddyDnsProvider.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Net.Http;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace LetsEncrypt.Azure.Core.V2.DnsProviders
9 | {
10 | public class GoDaddyDnsProvider : IDnsProvider
11 | {
12 | private readonly HttpClient httpClient;
13 |
14 | public GoDaddyDnsProvider(GoDaddyDnsSettings settings)
15 | {
16 | this.httpClient = new HttpClient();
17 | this.httpClient.BaseAddress = new Uri($"https://api.godaddy.com/v1/domains/{settings.Domain}/");
18 | this.httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"sso-key {settings.ApiKey}:{settings.ApiSecret}");
19 | this.httpClient.DefaultRequestHeaders.TryAddWithoutValidation("X-Shopper-Id", settings.ShopperId);
20 | }
21 |
22 | public int MinimumTtl => 600;
23 |
24 | public Task Cleanup(string recordSetName)
25 | {
26 | return Task.FromResult(0);
27 | }
28 |
29 | public async Task PersistChallenge(string recordSetName, string recordValue)
30 | {
31 | var body = await httpClient.GetStringAsync($"records/TXT/{recordSetName}");
32 | var acmeChallengeRecord = JsonConvert.DeserializeObject(body);
33 |
34 |
35 | acmeChallengeRecord = new[]{new DnsRecord
36 | {
37 | data = recordValue,
38 | name = recordSetName,
39 | ttl = MinimumTtl,
40 | type = "TXT"
41 | }};
42 |
43 | var res = await this.httpClient.PutAsync($"records/TXT/{recordSetName}", new StringContent(JsonConvert.SerializeObject(acmeChallengeRecord), Encoding.UTF8, "application/json"));
44 | body = await res.Content.ReadAsStringAsync();
45 | res.EnsureSuccessStatusCode();
46 |
47 | }
48 |
49 | public class GoDaddyDnsSettings
50 | {
51 | public string ApiKey { get; set; }
52 | public string ApiSecret { get; set; }
53 | public string ShopperId { get; set; }
54 | public string Domain { get; set; }
55 | }
56 |
57 |
58 | public class DnsRecord
59 | {
60 | public string data { get; set; }
61 | public string name { get; set; }
62 | public int ttl { get; set; }
63 | public string type { get; set; }
64 | }
65 |
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/DnsProviders/IDnsProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace LetsEncrypt.Azure.Core.V2.DnsProviders
4 | {
5 | public interface IDnsProvider
6 | {
7 | Task PersistChallenge(string recordSetName, string recordValue);
8 | Task Cleanup(string recordSetName);
9 |
10 | ///
11 | /// The minimum ttl value in seconds, that the provider supports.
12 | ///
13 | int MinimumTtl { get; }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/DnsProviders/UnoEuroDnsProvider.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace LetsEncrypt.Azure.Core.V2.DnsProviders
9 | {
10 | public class UnoEuroDnsProvider : IDnsProvider
11 | {
12 | private readonly HttpClient httpClient;
13 |
14 | public int MinimumTtl => 1200; //Minimum is 600, but their dns servers are quite slow at updating so give some extra time.
15 |
16 | public UnoEuroDnsProvider(UnoEuroDnsSettings settings)
17 | {
18 | this.httpClient = new HttpClient();
19 | httpClient.BaseAddress = new Uri($"https://api.unoeuro.com/1/{settings.AccountName}/{settings.ApiKey}/my/products/{settings.Domain}/dns/records/");
20 | }
21 |
22 | public async Task Cleanup(string recordSetName)
23 | {
24 | DnsRecord acmeChallengeRecord = await GetRecord(recordSetName);
25 | if (acmeChallengeRecord != null)
26 | {
27 | var res = await this.httpClient.DeleteAsync($"{acmeChallengeRecord.record_id}");
28 | res.EnsureSuccessStatusCode();
29 | }
30 | }
31 |
32 | public async Task PersistChallenge(string recordSetName, string recordValue)
33 | {
34 | DnsRecord acmeChallengeRecord = await GetRecord(recordSetName);
35 |
36 | if (acmeChallengeRecord != null)
37 | {
38 | //Update
39 | var update = new
40 | {
41 | acmeChallengeRecord.type,
42 | acmeChallengeRecord.ttl,
43 | acmeChallengeRecord.name,
44 | data = recordValue,
45 | acmeChallengeRecord.priority
46 | };
47 | StringContent content = CreateRequestBody(update);
48 | var res = await httpClient.PutAsync($"{acmeChallengeRecord.record_id}", content);
49 | var s = res.Content.ReadAsStringAsync();
50 | res.EnsureSuccessStatusCode();
51 | }
52 | else
53 | {
54 | acmeChallengeRecord = new DnsRecord()
55 | {
56 | ttl = MinimumTtl,
57 | type = "TXT",
58 | name = recordSetName,
59 | data = recordValue,
60 | priority = 0
61 | };
62 | //Create
63 | var res = await httpClient.PostAsync("", CreateRequestBody(acmeChallengeRecord));
64 | res.EnsureSuccessStatusCode();
65 | }
66 | }
67 |
68 | ///
69 | /// Create the request body, uno euro doesn't support charset in the content-type.
70 | ///
71 | ///
72 | ///
73 | private static StringContent CreateRequestBody(object body)
74 | {
75 | var content = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
76 | content.Headers.ContentType.CharSet = string.Empty;
77 | return content;
78 | }
79 |
80 | private async Task GetRecord(string recordSetName)
81 | {
82 | var records = JsonConvert.DeserializeObject(await this.httpClient.GetStringAsync(""));
83 |
84 | var acmeChallengeRecord = records.records.FirstOrDefault(s => s.type == "TXT" && s.name == recordSetName);
85 | return acmeChallengeRecord;
86 | }
87 |
88 | public class DnsResponse
89 | {
90 | public DnsRecord[] records { get; set; }
91 | public string message { get; set; }
92 | public int status { get; set; }
93 | }
94 |
95 | public class DnsRecord
96 | {
97 | public int? record_id { get; set; }
98 | public string name { get; set; }
99 | public int ttl { get; set; }
100 | public string data { get; set; }
101 | public string type { get; set; }
102 | public int priority { get; set; }
103 | }
104 | }
105 |
106 | public class UnoEuroDnsSettings
107 | {
108 | public string AccountName { get; set; }
109 | public string ApiKey { get; set; }
110 | public string Domain { get; set; }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/FileSystem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace LetsEncrypt.Azure.Core.V2
8 | {
9 | public class FileSystem : IFileSystem
10 | {
11 | public Task Exists(string v)
12 | {
13 | return Task.FromResult(File.Exists(v));
14 | }
15 |
16 | public Task Read(string v)
17 | {
18 | return Task.FromResult(File.ReadAllBytes(v));
19 | }
20 |
21 | public Task ReadAllText(string v)
22 | {
23 | return Task.FromResult(File.ReadAllText(v));
24 | }
25 |
26 | public Task Write(string v, byte[] data)
27 | {
28 | File.WriteAllBytes(v, data);
29 | return Task.FromResult(0);
30 | }
31 |
32 | public Task WriteAllText(string v, string pemKey)
33 | {
34 | File.WriteAllText(v, pemKey);
35 | return Task.FromResult(0);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/IFileSystem.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace LetsEncrypt.Azure.Core
4 | {
5 | public interface IFileSystem
6 | {
7 | Task Exists(string v);
8 | Task WriteAllText(string v, string pemKey);
9 | Task ReadAllText(string v);
10 |
11 | Task Read(string v);
12 | Task Write(string v, byte[] data);
13 | }
14 | }
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/LetsEncrypt.Azure.Core.V2.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Library for easy retrieval of Let's Encrypt wildcard certificates using version 2 api. Support easy install to Azure Web Apps and storage in Azure Key Vault or Blob Storage.
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6\System.ComponentModel.DataAnnotations.dll
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/LetsencryptService.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2.CertificateConsumers;
2 | using LetsEncrypt.Azure.Core.V2.CertificateStores;
3 | using LetsEncrypt.Azure.Core.V2.Models;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Logging.Abstractions;
6 | using System;
7 | using System.Threading.Tasks;
8 |
9 | namespace LetsEncrypt.Azure.Core.V2
10 | {
11 | public class LetsencryptService
12 | {
13 | private readonly AcmeClient acmeClient;
14 | private readonly ICertificateStore certificateStore;
15 | private readonly ICertificateConsumer certificateConsumer;
16 | private readonly ILogger logger;
17 |
18 | public LetsencryptService(AcmeClient acmeClient, ICertificateStore certificateStore, ICertificateConsumer certificateConsumer, ILogger logger = null)
19 | {
20 | this.acmeClient = acmeClient;
21 | this.certificateStore = certificateStore;
22 | this.certificateConsumer = certificateConsumer;
23 | this.logger = logger ?? NullLogger.Instance;
24 | }
25 | public async Task Run(AcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBeforeExpiration)
26 | {
27 | try
28 | {
29 | CertificateInstallModel model = null;
30 |
31 | var certname = acmeDnsRequest.Host.Substring(2) + "-" + acmeDnsRequest.AcmeEnvironment.Name;
32 | var cert = await certificateStore.GetCertificate(certname, acmeDnsRequest.PFXPassword);
33 | if (cert == null || cert.Certificate.NotAfter < DateTime.UtcNow.AddDays(renewXNumberOfDaysBeforeExpiration)) //Cert doesnt exist or expires in less than renewXNumberOfDaysBeforeExpiration days, lets renew.
34 | {
35 | logger.LogInformation("Certificate store didn't contain certificate or certificate was expired starting renewing");
36 | model = await acmeClient.RequestDnsChallengeCertificate(acmeDnsRequest);
37 | model.CertificateInfo.Name = certname;
38 | await certificateStore.SaveCertificate(model.CertificateInfo);
39 | }
40 | else
41 | {
42 | logger.LogInformation("Certificate expires in more than {renewXNumberOfDaysBeforeExpiration} days, reusing certificate from certificate store", renewXNumberOfDaysBeforeExpiration);
43 | model = new CertificateInstallModel()
44 | {
45 | CertificateInfo = cert,
46 | Host = acmeDnsRequest.Host
47 | };
48 | }
49 | await certificateConsumer.Install(model);
50 |
51 | logger.LogInformation("Removing expired certificates");
52 | var expired = await certificateConsumer.CleanUp();
53 | logger.LogInformation("The following certificates was removed {Thumbprints}", string.Join(", ", expired.ToArray()));
54 |
55 | }
56 | catch (Exception e)
57 | {
58 | logger.LogError(e, "Failed");
59 | throw;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/LetsencryptServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using LetsEncrypt.Azure.Core.V2.CertificateConsumers;
2 | using LetsEncrypt.Azure.Core.V2.CertificateStores;
3 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
4 | using LetsEncrypt.Azure.Core.V2.Models;
5 | using Microsoft.Azure.KeyVault;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using System;
8 | using System.Linq;
9 |
10 | namespace LetsEncrypt.Azure.Core.V2
11 | {
12 | public static class LetsencryptServiceCollectionExtensions
13 | {
14 | public static IServiceCollection AddAzureBlobStorageCertificateStore(this IServiceCollection serviceCollection, string azureStorageConnectionString)
15 | {
16 | return serviceCollection
17 | .AddTransient(s =>
18 | {
19 | return new AzureBlobStorage(azureStorageConnectionString);
20 | })
21 | .AddTransient(s =>
22 | {
23 | return new AzureBlobStorage(azureStorageConnectionString);
24 | })
25 | .AddTransient();
26 | }
27 |
28 | public static IServiceCollection AddKeyVaultCertificateStore(this IServiceCollection serviceCollection, string vaultBaseUrl)
29 | {
30 | return serviceCollection
31 | .AddTransient((serviceProvider) =>
32 | {
33 | return new AzureKeyVaultCertificateStore(serviceProvider.GetService(), vaultBaseUrl);
34 | });
35 | }
36 |
37 | public static IServiceCollection AddFileSystemCertificateStore(this IServiceCollection serviceCollection)
38 | {
39 | return serviceCollection
40 | .AddTransient()
41 | .AddTransient();
42 | }
43 |
44 | public static IServiceCollection AddAcmeClient(this IServiceCollection serviceCollection, object dnsProviderConfig) where TDnsProvider : class, IDnsProvider
45 | {
46 | if (serviceCollection == null)
47 | {
48 | throw new ArgumentNullException(nameof(serviceCollection));
49 | }
50 |
51 | if (dnsProviderConfig == null)
52 | {
53 | throw new ArgumentNullException(nameof(dnsProviderConfig));
54 | }
55 |
56 | if (!serviceCollection.Any(s => s.ServiceType == typeof(ICertificateStore)))
57 | {
58 | serviceCollection.AddTransient();
59 | }
60 |
61 | return serviceCollection
62 | .AddTransient()
63 | .AddTransient()
64 | .AddSingleton(dnsProviderConfig.GetType(), dnsProviderConfig)
65 | .AddTransient();
66 | }
67 |
68 | public static IServiceCollection AddNullCertificateConsumer(this IServiceCollection serviceCollection)
69 | {
70 |
71 | return serviceCollection
72 | .AddTransient()
73 | .AddTransient();
74 | }
75 |
76 |
77 |
78 | public static IServiceCollection AddAzureAppService(this IServiceCollection serviceCollection, params AzureWebAppSettings[] settings)
79 | {
80 | if (settings == null || settings.Length == 0)
81 | {
82 | throw new ArgumentNullException(nameof(settings));
83 | }
84 |
85 | return serviceCollection
86 | .AddSingleton(settings)
87 | .AddTransient()
88 | .AddTransient();
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/MessageHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using System.Net.Http;
7 | using System.Text;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace LetsEncrypt.Azure.Core.V2
12 | {
13 | public abstract class MessageHandler : DelegatingHandler
14 | {
15 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
16 | {
17 | var corrId = string.Format("{0}{1}", DateTime.Now.Ticks, Thread.CurrentThread.ManagedThreadId);
18 | var requestInfo = string.Format("{0} {1}, headers {2}", request.Method, request.RequestUri, string.Join(",", request.Headers
19 | .Where(s => !string.Equals(s.Key, "Authorization", StringComparison.InvariantCultureIgnoreCase))
20 | .Select(s => $"{s.Key} = {string.Join("|", s.Value)}")
21 | ));
22 |
23 | byte[] requestMessage = null;
24 | if (request.Content != null)
25 | {
26 | requestMessage = await request.Content.ReadAsByteArrayAsync();
27 | }
28 |
29 | await IncommingMessageAsync(corrId, requestInfo, requestMessage);
30 |
31 | var response = await base.SendAsync(request, cancellationToken);
32 |
33 | byte[] responseMessage = null;
34 | if (response.Content != null)
35 | {
36 | responseMessage = await response.Content.ReadAsByteArrayAsync();
37 | }
38 |
39 | await OutgoingMessageAsync(corrId, requestInfo, responseMessage);
40 |
41 | return response;
42 | }
43 |
44 |
45 | protected abstract Task IncommingMessageAsync(string correlationId, string requestInfo, byte[] message);
46 | protected abstract Task OutgoingMessageAsync(string correlationId, string requestInfo, byte[] message);
47 | }
48 |
49 |
50 |
51 | public class MessageLoggingHandler : MessageHandler
52 | {
53 | private readonly ILogger logger;
54 |
55 | public MessageLoggingHandler(ILogger logger)
56 | {
57 | this.InnerHandler = new HttpClientHandler();
58 | this.logger = logger;
59 | }
60 | protected override async Task IncommingMessageAsync(string correlationId, string requestInfo, byte[] message)
61 | {
62 | await Task.Run(() =>
63 | logger.LogInformation(string.Format("{0} - Request: {1}\r\n{2}", correlationId, requestInfo, message != null ? Encoding.UTF8.GetString(message) : String.Empty)));
64 | }
65 |
66 |
67 | protected override async Task OutgoingMessageAsync(string correlationId, string requestInfo, byte[] message)
68 | {
69 | await Task.Run(() =>
70 | logger.LogInformation(string.Format("{0} - Response: {1}\r\n{2}", correlationId, requestInfo, message != null ? Encoding.UTF8.GetString(message) : String.Empty)));
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/Models/AcmeDnsRequest.cs:
--------------------------------------------------------------------------------
1 | using Certes.Acme;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace LetsEncrypt.Azure.Core.V2.Models
7 | {
8 | public class AcmeDnsRequest : IAcmeDnsRequest
9 | {
10 | ///
11 | /// The email to register with lets encrypt with. Will recieve notifications on expiring certificates.
12 | ///
13 | public string RegistrationEmail { get; set; }
14 |
15 | ///
16 | /// The ACME environment, use or or provide you own ACME compatible endpoint by implementing .
17 | ///
18 | public AcmeEnvironment AcmeEnvironment { get; set; }
19 |
20 | ///
21 | /// The host name to request a certificate for e.g. *.example.com
22 | ///
23 | public string Host { get; set; }
24 |
25 | public string PFXPassword { get; set; }
26 |
27 | public CsrInfo CsrInfo { get; set; }
28 | }
29 |
30 | public interface IAcmeDnsRequest
31 | {
32 | ///
33 | /// The email to register with lets encrypt with. Will recieve notifications on expiring certificates.
34 | ///
35 | string RegistrationEmail { get; }
36 | ///
37 | /// The ACME environment, use or or provide you own ACME compatible endpoint by implementing .
38 | ///
39 | AcmeEnvironment AcmeEnvironment { get; }
40 |
41 | ///
42 | /// The host name to request a certificate for e.g. *.example.com
43 | ///
44 | string Host { get; }
45 |
46 | string PFXPassword { get; }
47 |
48 | CsrInfo CsrInfo { get; }
49 | }
50 |
51 | public class CsrInfo
52 | {
53 | public string CountryName { get; set; }
54 | public string State { get; set; }
55 | public string Locality { get; set; }
56 | public string Organization { get; set; }
57 | public string OrganizationUnit { get; set; }
58 | }
59 |
60 | public class AcmeEnvironment
61 | {
62 | public Uri BaseUri { get; set; }
63 |
64 | public AcmeEnvironment()
65 | {
66 |
67 | }
68 |
69 | public AcmeEnvironment(Uri uri)
70 | {
71 | this.BaseUri = uri;
72 | }
73 | protected string name;
74 | public string Name
75 | {
76 | get
77 | {
78 | return name;
79 | }
80 | set
81 | {
82 | if ("production".Equals(value, StringComparison.InvariantCultureIgnoreCase))
83 | {
84 | BaseUri = WellKnownServers.LetsEncryptV2;
85 | }
86 | else
87 | {
88 | BaseUri = WellKnownServers.LetsEncryptStagingV2;
89 | }
90 | name = value;
91 | }
92 | }
93 | }
94 |
95 |
96 | public class LetsEncryptStagingV2 : AcmeEnvironment
97 | {
98 | public LetsEncryptStagingV2() : base(WellKnownServers.LetsEncryptStagingV2)
99 | {
100 | this.name = "staging";
101 | }
102 | }
103 |
104 | public class LetsEncryptV2 : AcmeEnvironment
105 | {
106 | public LetsEncryptV2() : base(WellKnownServers.LetsEncryptV2)
107 | {
108 | this.name = "production";
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/Models/AzureDnsSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace LetsEncrypt.Azure.Core.V2.Models
6 | {
7 | public class AzureDnsSettings
8 | {
9 | public AzureDnsSettings()
10 | {
11 | this.RelativeRecordSetName = "@";
12 | }
13 |
14 | public AzureDnsSettings(string resourceGroupName, string zoneName, AzureServicePrincipal servicePrincipal, AzureSubscription azureSubscription, string relativeRecordName = "@")
15 | {
16 | this.AzureSubscription = azureSubscription;
17 | this.AzureServicePrincipal = servicePrincipal;
18 | this.ResourceGroupName = resourceGroupName;
19 | this.ZoneName = zoneName;
20 | this.RelativeRecordSetName = resourceGroupName;
21 | }
22 |
23 | public AzureServicePrincipal AzureServicePrincipal {get;set;}
24 | public AzureSubscription AzureSubscription { get; set; }
25 |
26 | public string ResourceGroupName { get; set; }
27 |
28 | public string RelativeRecordSetName { get; set; }
29 |
30 | public string ZoneName { get; set; }
31 |
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/Models/AzureServicePrincipal.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Management.ResourceManager.Fluent.Authentication;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace LetsEncrypt.Azure.Core.V2.Models
7 | {
8 | public class AzureServicePrincipal
9 | {
10 | public bool UseManagendIdentity { get; set; }
11 | public string ClientId { get; set; }
12 | public string ClientSecret { get; set; }
13 | public byte[] Certificate { get; set; }
14 | public string CertificatePassword { get; set; }
15 |
16 | internal ServicePrincipalLoginInformation ServicePrincipalLoginInformation => new ServicePrincipalLoginInformation()
17 | {
18 | Certificate = this.Certificate,
19 | ClientId = this.ClientId,
20 | ClientSecret = this.ClientSecret,
21 | CertificatePassword = this.CertificatePassword
22 | };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/Models/AzureSubscription.cs:
--------------------------------------------------------------------------------
1 | namespace LetsEncrypt.Azure.Core.V2.Models
2 | {
3 | public class AzureSubscription
4 | {
5 | public string Tenant { get; set; }
6 | public string SubscriptionId { get; set; }
7 |
8 | ///
9 | /// Should be AzureGlobalCloud, AzureChinaCloud, AzureUSGovernment or AzureGermanCloud
10 | ///
11 | public string AzureRegion { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/Models/AzureWebAppSettings.cs:
--------------------------------------------------------------------------------
1 | namespace LetsEncrypt.Azure.Core.V2.Models
2 | {
3 | public class AzureWebAppSettings
4 | {
5 | public AzureWebAppSettings()
6 | {
7 |
8 | }
9 | public AzureWebAppSettings(string webappName, string resourceGroup, AzureServicePrincipal servicePrincipal, AzureSubscription azureSubscription, string siteSlotName = null, string servicePlanResourceGroupName = null, bool useIPBasedSSL = false)
10 | {
11 | this.WebAppName = webappName;
12 | this.ResourceGroupName = resourceGroup;
13 | this.AzureServicePrincipal = servicePrincipal;
14 | this.AzureSubscription = azureSubscription;
15 | this.SiteSlotName = siteSlotName;
16 | this.ServicePlanResourceGroupName = servicePlanResourceGroupName;
17 | this.UseIPBasedSSL = useIPBasedSSL;
18 | }
19 | public string WebAppName { get; set; }
20 | public string ResourceGroupName { get; set; }
21 |
22 | public string ServicePlanResourceGroupName { get; set; }
23 |
24 | ///
25 | /// Returns service plan resource group name unless empty then returns resource group name.
26 | ///
27 | public string ServicePlanOrResourceGroupName
28 | {
29 | get
30 | {
31 | return string.IsNullOrEmpty(ServicePlanResourceGroupName) ? ResourceGroupName : ServicePlanResourceGroupName;
32 | }
33 | }
34 |
35 | public string SiteSlotName { get; set; }
36 |
37 | public bool UseIPBasedSSL { get; set; }
38 |
39 | public AzureServicePrincipal AzureServicePrincipal { get; set; }
40 |
41 | public AzureSubscription AzureSubscription { get; set; }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/Models/BlobCertificateStoreAppSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace LetsEncrypt.Azure.Core.V2.Models
6 | {
7 | public class BlobCertificateStoreAppSettings
8 | {
9 | public string ConnectionString { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/Models/CertificateInfo.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Security.Cryptography.X509Certificates;
5 | using System.Text;
6 |
7 | namespace LetsEncrypt.Azure.Core.V2.Models
8 | {
9 | ///
10 | /// Information about the requested certificate.
11 | ///
12 | public class CertificateInfo
13 | {
14 | [JsonIgnore]
15 | public X509Certificate2 Certificate { get; set; }
16 | ///
17 | /// The name of the certificate.
18 | ///
19 | public string Name { get; set; }
20 |
21 | ///
22 | /// Password of the certificate.
23 | ///
24 | public string Password { get; set; }
25 |
26 | ///
27 | /// The byte content of the pfx certificate.
28 | ///
29 | public byte[] PfxCertificate { get; set; }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/Models/CertificateInstallModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace LetsEncrypt.Azure.Core.V2.Models
6 | {
7 | ///
8 | /// Result of the certificate installation.
9 | ///
10 | public class CertificateInstallModel : ICertificateInstallModel
11 | {
12 | ///
13 | /// Certificate info.
14 | ///
15 | public CertificateInfo CertificateInfo
16 | {
17 | get; set;
18 | }
19 |
20 | ///
21 | /// The primary host name.
22 | ///
23 | public string Host
24 | {
25 | get; set;
26 | }
27 | }
28 |
29 | public interface ICertificateInstallModel
30 | {
31 | CertificateInfo CertificateInfo { get; set; }
32 |
33 | string Host { get; set; }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Core.V2/Models/KeyVaultCertificateStoreAppSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace LetsEncrypt.Azure.Core.V2.Models
6 | {
7 | public class KeyVaultCertificateStoreAppSettings
8 | {
9 | public AzureServicePrincipal AzureServicePrincipal { get; set; }
10 |
11 | public AzureSubscription AzureSubscription { get; set; }
12 |
13 | public string BaseUrl { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 14
4 | VisualStudioVersion = 14.0.25420.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}") = "LetsEncrypt.Azure.ResourceGroup", "LetsEncrypt.Azure.ResourceGroup\LetsEncrypt.Azure.ResourceGroup.deployproj", "{18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {18992D22-E4F4-4A82-B1C9-18BFA3D3BFE7}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/ArmClient/getWebApp.request:
--------------------------------------------------------------------------------
1 | ARMClient GET /subscriptions/14fe4c66-c75a-4323-881b-ea53c1d86a9d/resourceGroups/LetsEncrypt.Azure.WebAppTest/providers/Microsoft.Web/sites/webApp6ozs5s2gy3wkg?api-version=2016-03-01
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/ArmClient/keyVaultCert.json:
--------------------------------------------------------------------------------
1 | {
2 | "Location": "northeurope",
3 | "Properties": {
4 | "KeyVaultSecretName": "keyVaultCert",
5 | "KeyVaultId": "/subscriptions/14fe4c66-c75a-4323-881b-ea53c1d86a9d/resourceGroups/LetsEncrypt.Azure/providers/Microsoft.KeyVault/vaults/sjkptest"
6 | }
7 | }
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/ArmClient/putKeyVaultCertificate.request:
--------------------------------------------------------------------------------
1 | ARMClient.exe PUT /subscriptions/14fe4c66-c75a-4323-881b-ea53c1d86a9d/resourceGroups/LetsEncrypt.Azure.WebAppTest/providers/Microsoft.Web/certificates/keyvaultcertificate?api-version=2016-03-01 @keyVaultCert.json
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Deploy-AzureResourceGroup.ps1:
--------------------------------------------------------------------------------
1 | #Requires -Version 3.0
2 | #Requires -Module AzureRM.Resources
3 | #Requires -Module Azure.Storage
4 |
5 | Param(
6 | [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation,
7 | [string] $ResourceGroupName = 'LetsEncrypt.Azure.ResourceGroup',
8 | [switch] $UploadArtifacts,
9 | [string] $StorageAccountName,
10 | [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts',
11 | [string] $TemplateFile = 'Templates\azuredeploy.json',
12 | [string] $TemplateParametersFile = 'Templates\azuredeploy.parameters.json',
13 | [string] $ArtifactStagingDirectory = '.',
14 | [string] $DSCSourceFolder = 'DSC'
15 | )
16 |
17 | Import-Module Azure -ErrorAction SilentlyContinue
18 |
19 | try {
20 | [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(" ","_"), "2.9.1")
21 | } catch { }
22 |
23 | Set-StrictMode -Version 3
24 |
25 | $OptionalParameters = New-Object -TypeName Hashtable
26 | $TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile))
27 | $TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile))
28 |
29 | if ($UploadArtifacts) {
30 | # Convert relative paths to absolute paths if needed
31 | $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory))
32 | $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder))
33 |
34 | Set-Variable ArtifactsLocationName '_artifactsLocation' -Option ReadOnly -Force
35 | Set-Variable ArtifactsLocationSasTokenName '_artifactsLocationSasToken' -Option ReadOnly -Force
36 |
37 | $OptionalParameters.Add($ArtifactsLocationName, $null)
38 | $OptionalParameters.Add($ArtifactsLocationSasTokenName, $null)
39 |
40 | # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present
41 | $JsonContent = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json
42 | $JsonParameters = $JsonContent | Get-Member -Type NoteProperty | Where-Object {$_.Name -eq "parameters"}
43 |
44 | if ($JsonParameters -eq $null) {
45 | $JsonParameters = $JsonContent
46 | }
47 | else {
48 | $JsonParameters = $JsonContent.parameters
49 | }
50 |
51 | $JsonParameters | Get-Member -Type NoteProperty | ForEach-Object {
52 | $ParameterValue = $JsonParameters | Select-Object -ExpandProperty $_.Name
53 |
54 | if ($_.Name -eq $ArtifactsLocationName -or $_.Name -eq $ArtifactsLocationSasTokenName) {
55 | $OptionalParameters[$_.Name] = $ParameterValue.value
56 | }
57 | }
58 |
59 | # Create DSC configuration archive
60 | if (Test-Path $DSCSourceFolder) {
61 | $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter "*.ps1" | ForEach-Object -Process {$_.FullName})
62 | foreach ($DSCSourceFilePath in $DSCSourceFilePaths) {
63 | $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + ".zip"
64 | Publish-AzureRmVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose
65 | }
66 | }
67 |
68 | # Create a storage account name if none was provided
69 | if($StorageAccountName -eq "") {
70 | $subscriptionId = ((Get-AzureRmContext).Subscription.SubscriptionId).Replace('-', '').substring(0, 19)
71 | $StorageAccountName = "stage$subscriptionId"
72 | }
73 |
74 | $StorageAccount = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName})
75 |
76 | # Create the storage account if it doesn't already exist
77 | if($StorageAccount -eq $null){
78 | $StorageResourceGroupName = "ARM_Deploy_Staging"
79 | New-AzureRmResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force
80 | $StorageAccount = New-AzureRmStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation"
81 | }
82 |
83 | $StorageAccountContext = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName}).Context
84 |
85 | # Generate the value for artifacts location if it is not provided in the parameter file
86 | $ArtifactsLocation = $OptionalParameters[$ArtifactsLocationName]
87 | if ($ArtifactsLocation -eq $null) {
88 | $ArtifactsLocation = $StorageAccountContext.BlobEndPoint + $StorageContainerName
89 | $OptionalParameters[$ArtifactsLocationName] = $ArtifactsLocation
90 | }
91 |
92 | # Copy files from the local storage staging location to the storage account container
93 | New-AzureStorageContainer -Name $StorageContainerName -Context $StorageAccountContext -Permission Container -ErrorAction SilentlyContinue *>&1
94 |
95 | $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName}
96 | foreach ($SourcePath in $ArtifactFilePaths) {
97 | $BlobName = $SourcePath.Substring($ArtifactStagingDirectory.length + 1)
98 | Set-AzureStorageBlobContent -File $SourcePath -Blob $BlobName -Container $StorageContainerName -Context $StorageAccountContext -Force
99 | }
100 |
101 | # Generate the value for artifacts location SAS token if it is not provided in the parameter file
102 | $ArtifactsLocationSasToken = $OptionalParameters[$ArtifactsLocationSasTokenName]
103 | if ($ArtifactsLocationSasToken -eq $null) {
104 | # Create a SAS token for the storage container - this gives temporary read-only access to the container
105 | $ArtifactsLocationSasToken = New-AzureStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccountContext -Permission r -ExpiryTime (Get-Date).AddHours(4)
106 | $ArtifactsLocationSasToken = ConvertTo-SecureString $ArtifactsLocationSasToken -AsPlainText -Force
107 | $OptionalParameters[$ArtifactsLocationSasTokenName] = $ArtifactsLocationSasToken
108 | }
109 | }
110 |
111 | # Create or update the resource group using the specified template file and template parameters file
112 | New-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force -ErrorAction Stop
113 |
114 | New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) `
115 | -ResourceGroupName $ResourceGroupName `
116 | -TemplateFile $TemplateFile `
117 | -TemplateParameterFile $TemplateParametersFile `
118 | @OptionalParameters `
119 | -Force -Verbose
120 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Deployment.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | bin\$(Configuration)\
7 | false
8 | true
9 | false
10 | None
11 | obj\
12 | $(BaseIntermediateOutputPath)\
13 | $(BaseIntermediateOutputPath)$(Configuration)\
14 | $(IntermediateOutputPath)ProjectReferences
15 | $(ProjectReferencesOutputPath)\
16 | true
17 |
18 |
19 |
20 | false
21 | false
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Always
33 |
34 |
35 | Never
36 |
37 |
38 | false
39 | Build
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | _GetDeploymentProjectContent;
48 | _CalculateContentOutputRelativePaths;
49 | _GetReferencedProjectsOutput;
50 | _CalculateArtifactStagingDirectory;
51 | _CopyOutputToArtifactStagingDirectory;
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Configuration=$(Configuration);Platform=$(Platform)
69 |
70 |
71 |
75 |
76 |
77 |
78 | $([System.IO.Path]::GetFileNameWithoutExtension('%(ProjectReference.Identity)'))
79 |
80 |
81 |
82 |
83 |
84 |
85 | $(OutDir)
86 | $(OutputPath)
87 | $(ArtifactStagingDirectory)\
88 | $(ArtifactStagingDirectory)staging\
89 | $(Build_StagingDirectory)
90 |
91 |
92 |
93 |
94 |
96 |
97 | <_OriginalIdentity>%(DeploymentProjectContentOutput.Identity)
98 | <_RelativePath>$(_OriginalIdentity.Replace('$(MSBuildProjectDirectory)', ''))
99 |
100 |
101 |
102 |
103 | $(_RelativePath)
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | PrepareForRun
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/LetsEncrypt.Azure.ResourceGroup.deployproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 |
8 |
9 | Release
10 | AnyCPU
11 |
12 |
13 |
14 | 18992d22-e4f4-4a82-b1c9-18bfa3d3bfe7
15 |
16 |
17 | Deployment
18 | 1.0
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | False
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/LetsEncrypt.Azure.ResourceGroup.deployproj.user:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 |
6 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Scripts/Create-CoreInfrastructure.ps1:
--------------------------------------------------------------------------------
1 | $tenantId = "37597dd5-5816-4d7a-99e8-b2e6c3f4d0c1"
2 | $subscriptionId = "cd0d7179-80a0-47e6-a201-9c12de0bbd37"
3 |
4 | $bytes = New-Object Byte[] 32
5 | $rand = [System.Security.Cryptography.RandomNumberGenerator]::Create()
6 | $rand.GetBytes($bytes)
7 | $rand.Dispose()
8 | $key = [System.Convert]::ToBase64String($bytes)
9 | Write-Host "service princpal password $key"
10 | $app = New-AzureRmADApplication -DisplayName "LetsEncrypt.Azure" -HomePage "https://letsencrypt-azure" -IdentifierUris "https://letsencrypt-azure" -Password $key
11 |
12 | $sp = New-AzureRmADServicePrincipal -ApplicationId $app.ApplicationId
13 |
14 | Write-Host $sp.Id
15 |
16 | $objectId = $sp.Id
17 |
18 | $resourceGroupName = "LetsEncrypt.Azure"
19 |
20 | New-AzureRmResourceGroup -Name $resourceGroupName -Location "NorthEurope"
21 |
22 | New-AzureRmResourceGroupDeployment -Name "test" -ResourceGroupName $resourceGroupName -TemplateParameterObject @{appName = "letsencryptfunctions"; keyVaultName = "letsencrypt-vault"; tenantId = $tenantId; objectId = $objectId} -TemplateFile .\..\Templates\letsencrypt.azure.core.json
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Scripts/Import-Certificate.ps1:
--------------------------------------------------------------------------------
1 | $pfxFilePath = $PSScriptRoot+"\KeyVault2_longer-expiration.pfx"
2 | Write-Host $pfxFilePath
3 |
4 | $pwd = "test"
5 | $flag = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
6 | $collection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
7 | $collection.Import($pfxFilePath, $pwd, $flag)
8 | $pkcs12ContentType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12
9 | $clearBytes = $collection.Export($pkcs12ContentType)
10 | $fileContentEncoded = [System.Convert]::ToBase64String($clearBytes)
11 | $secret = ConvertTo-SecureString -String $fileContentEncoded -AsPlainText –Force
12 | $secretContentType = 'application/x-pkcs12'
13 | Set-AzureKeyVaultSecret -VaultName sjkptest -Name keyVaultCert -SecretValue $secret -ContentType $secretContentType # Change the Key Vault name and secret name
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Scripts/KeyVault.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjkp/letsencrypt-azure/dbdc477a88569f3fc63e1865c669923aacbf2d7a/src/LetsEncrypt.Azure.ResourceGroup/Scripts/KeyVault.pfx
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Scripts/KeyVault2.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjkp/letsencrypt-azure/dbdc477a88569f3fc63e1865c669923aacbf2d7a/src/LetsEncrypt.Azure.ResourceGroup/Scripts/KeyVault2.pfx
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Scripts/KeyVault2_longer-expiration.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjkp/letsencrypt-azure/dbdc477a88569f3fc63e1865c669923aacbf2d7a/src/LetsEncrypt.Azure.ResourceGroup/Scripts/KeyVault2_longer-expiration.pfx
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Scripts/New-Certificate.ps1:
--------------------------------------------------------------------------------
1 | $cert = New-SelfSignedCertificate -CertStoreLocation "cert:\CurrentUser\My" -Subject "CN=exampleapp" -KeySpec KeyExchange
2 |
3 | $certContentEncoded = [System.Convert]::ToBase64String($cert.GetRawCertData())
4 | Write-Host $certContentEncoded
5 | $secret = ConvertTo-SecureString -String $certContentEncoded -AsPlainText –Force
6 | $secretContentType = 'application/x-pkcs12'
7 | Set-AzureKeyVaultSecret -VaultName sjkpvault2 -Name testCert -SecretValue $secret -ContentType $secretContentType # Change the Key Vault name and secret name
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "functionAppName": {
6 | "type": "string",
7 | "defaultValue": "[take(concat('letsencrypt-runner-', uniqueString(resourceGroup().id)),60)]",
8 | "metadata": {
9 | "description": "Globally unique name for the Let's Encrypt Renewer function app."
10 | }
11 | },
12 | "hostingPlanName": {
13 | "type": "string",
14 | "minLength": 1,
15 | "defaultValue": "[take(concat('letsencrypt-runner', uniqueString(resourceGroup().id)),60)]",
16 | "metadata": {
17 | "description": "Name used for the hosting plan"
18 | }
19 | },
20 | "vaultName": {
21 | "type": "string",
22 | "defaultValue": "[take(concat('letsencrypt-runnerr-', uniqueString(resourceGroup().id)),24)]",
23 | "metadata": {
24 | "description": "Globally unique name for the Key Vault used to store the certificate and secrets used by the Let's Encrypt Renewer function app"
25 | }
26 | },
27 | "targetWebAppResourceGroupName": {
28 | "type": "string",
29 | "metadata": {
30 | "description": "The resource group that contains the web app that should have the SSL cert assigned"
31 | }
32 | },
33 | "targetWebAppName": {
34 | "type": "string",
35 | "metadata": {
36 | "description": "The name of the web application that should have the SSL Cert assigned"
37 | }
38 | },
39 | "dnsZoneName": {
40 | "type": "string",
41 | "metadata": {
42 | "description": "The DNS zone name in azure, e.g. yourwebsite.com"
43 | }
44 | },
45 | "dnsResourceGroupName": {
46 | "type": "string",
47 | "metadata": {
48 | "description": "The name of the resource group that contains the Azure DNS service (must already exists)"
49 | }
50 | },
51 | "roleNameGuid": {
52 | "type": "string",
53 | "metadata": {
54 | "description": "A new GUID used to identify the role assignment made for the Service Principal added to the Web App Resource Group"
55 | },
56 | "defaultValue": "[newGuid()]"
57 | },
58 | "roleNameDnsGuid": {
59 | "type": "string",
60 | "metadata": {
61 | "description": "A new GUID used to identify the role assignment that made for the Service Principal added to the DNS resource group"
62 | },
63 | "defaultValue": "[newGuid()]"
64 | },
65 | "builtInRoleType": {
66 | "type": "string",
67 | "allowedValues": [
68 | "Owner",
69 | "Contributor",
70 | "Reader"
71 | ],
72 | "metadata": {
73 | "description": "Built-in role to assign"
74 | },
75 | "defaultValue": "Contributor"
76 | },
77 | "csrOrganisation": {
78 | "type": "string",
79 | "metadata": {
80 | "description": "Name of the organisation that owns the domain, will be present in the Certificate"
81 | }
82 | },
83 | "acmeRegistrationEmail": {
84 | "type": "string",
85 | "metadata": {
86 | "description": "Email used to register with Let's Encrypt, you will recieve certificate expiry notifications on this email"
87 | }
88 | },
89 | "acmeEnvironment": {
90 | "type": "string",
91 | "allowedValues": [
92 | "production",
93 | "staging"
94 | ],
95 | "metadata": {
96 | "description": "The environment to request certificates from"
97 | },
98 | "defaultValue": "staging"
99 | },
100 | "certificateDomain": {
101 | "type": "string",
102 | "metadata": {
103 | "description": "The domain name to request a certificate for e.g. *.yourdomain.com"
104 | }
105 | },
106 | "pfxPass": {
107 | "type": "string",
108 | "defaultValue": "[newGuid()]",
109 | "metadata": {
110 | "description": "The password used to protect the certificate (only used internally)"
111 | }
112 | },
113 | "runFromPackage": {
114 | "type": "string",
115 | "defaultValue": "https://letsencryptazure.blob.core.windows.net/releases/126.zip",
116 | "metadata": {
117 | "description": "Set this to a url for a deployment package or 0 for not deploying anything to the Azure Function. The Default value deploy the latest version of the Azure Function Renewer code"
118 | }
119 | },
120 | "certRenewSchedule": {
121 | "type": "string",
122 | "defaultValue": "0 0 3 * * *",
123 | "metadata": {
124 | "description": "Cron expression for when the certificate renewal job should run see examples here: https://github.com/atifaziz/NCrontab/wiki/Crontab-Expression. Defaults to run every day at 03.00 UTC"
125 | }
126 | }
127 | },
128 | "variables": {
129 | "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]",
130 | "storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]",
131 | "storageConnectionStringName": "blobStorageConnectionString",
132 | "storageAccountResourceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
133 | "appInsightsName": "[concat(uniquestring(resourceGroup().id), 'appinsight')]",
134 | "pfxPass": "pfxPassword",
135 | "Owner": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]",
136 | "Contributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]",
137 | "Reader": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]",
138 | "scope": "[concat(subscription().id, '/resourceGroups/', parameters('targetWebAppResourceGroupName'))]",
139 | "scopeDns": "[concat(subscription().id, '/resourceGroups/', parameters('dnsResourceGroupName'))]"
140 | },
141 | "resources": [
142 | {
143 | "name": "[parameters('vaultName')]",
144 | "type": "Microsoft.KeyVault/vaults",
145 | "apiVersion": "2018-02-14",
146 | "location": "[resourceGroup().location]",
147 | "tags": {},
148 | "dependsOn": [
149 | "[resourceId('Microsoft.Web/sites/', parameters('functionAppName'))]"
150 | ],
151 | "properties": {
152 | "tenantId": "[subscription().tenantId]",
153 | "sku": {
154 | "family": "A",
155 | "name": "standard"
156 | },
157 | "accessPolicies": [
158 | {
159 | "tenantId": "[reference(concat('Microsoft.Web/sites/', parameters('functionAppName')), '2018-02-01', 'Full').identity.tenantId]",
160 | "objectId": "[reference(concat('Microsoft.Web/sites/', parameters('functionAppName')), '2018-02-01', 'Full').identity.principalId]",
161 | "permissions": {
162 | "keys": [],
163 | "secrets": [
164 | "get",
165 | "set",
166 | "list"
167 | ],
168 | "certificates": [
169 | "get",
170 | "list",
171 | "import",
172 | "update"
173 | ],
174 | "storage": []
175 | }
176 | }
177 | ],
178 | "enabledForTemplateDeployment": true
179 | },
180 | "resources": [
181 | {
182 | "type": "secrets",
183 | "name": "[variables('storageConnectionStringName')]",
184 | "apiVersion": "2018-02-14",
185 | "dependsOn": [
186 | "[resourceId('Microsoft.KeyVault/vaults/', parameters('vaultName'))]",
187 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
188 | ],
189 | "properties": {
190 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountResourceId'),'2015-05-01-preview').key1)]"
191 | }
192 | },
193 | {
194 | "type": "secrets",
195 | "name": "[variables('pfxPass')]",
196 | "apiVersion": "2018-02-14",
197 | "dependsOn": [
198 | "[resourceId('Microsoft.KeyVault/vaults/', parameters('vaultName'))]"
199 | ],
200 | "properties": {
201 | "value": "[parameters('pfxPass')]"
202 | }
203 | }
204 | ]
205 | },
206 | {
207 | "type": "Microsoft.Web/serverfarms",
208 | "apiVersion": "2015-04-01",
209 | "name": "[parameters('hostingPlanName')]",
210 | "location": "[resourceGroup().location]",
211 | "properties": {
212 | "name": "[parameters('hostingPlanName')]",
213 | "computeMode": "Dynamic",
214 | "sku": "Dynamic"
215 | }
216 | },
217 | {
218 | "apiVersion": "2015-08-01",
219 | "name": "[parameters('functionAppName')]",
220 | "type": "Microsoft.Web/sites",
221 | "kind": "functionapp",
222 | "location": "[resourceGroup().location]",
223 | "identity": {
224 | "type": "SystemAssigned"
225 | },
226 | "dependsOn": [
227 | "[resourceId('Microsoft.Web/serverFarms/', parameters('hostingPlanName'))]",
228 | "[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
229 | ],
230 | "tags": {
231 | "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "empty",
232 | "displayName": "Website"
233 | },
234 | "properties": {
235 | "name": "[parameters('functionAppName')]",
236 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]"
237 | },
238 | "resources": [
239 | {
240 | "name": "appsettings",
241 | "type": "config",
242 | "apiVersion": "2015-08-01",
243 | "dependsOn": [
244 | "[resourceId('Microsoft.Web/Sites/', parameters('functionAppName'))]",
245 | "[resourceId('Microsoft.Insights/components/', variables('appInsightsName'))]"
246 | ],
247 | "tags": {
248 | "displayName": "appSettings"
249 | },
250 | "properties": {
251 | "AzureWebJobsDashboard": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', parameters('vaultName'), variables('storageConnectionStringName'))).secretUriWithVersion, ')')]",
252 | "AzureWebJobsStorage": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', parameters('vaultName'), variables('storageConnectionStringName'))).secretUriWithVersion, ')')]",
253 | "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', parameters('vaultName'), variables('storageConnectionStringName'))).secretUriWithVersion, ')')]",
254 | "WEBSITE_CONTENTSHARE": "[toLower(parameters('functionAppName'))]",
255 | "FUNCTIONS_EXTENSION_VERSION": "~2",
256 | "FUNCTIONS_WORKER_RUNTIME": "dotnet",
257 | "Vault": "[parameters('vaultName')]",
258 | "AzureAppService__WebAppName": "[parameters('targetWebAppName')]",
259 | "AzureAppService__ResourceGroupName": "[parameters('targetWebAppResourceGroupName')]",
260 | "AzureAppService__AzureServicePrincipal__UseManagendIdentity": "true",
261 | "AzureAppService__AzureSubscription__Tenant": "[subscription().tenantId]",
262 | "AzureAppService__AzureSubscription__SubscriptionId": "[subscription().subscriptionId]",
263 | "AzureAppService__AzureSubscription__AzureRegion": "AzureGlobalCloud",
264 | "DnsSettings__ZoneName": "[parameters('dnsZoneName')]",
265 | "DnsSettings__ResourceGroupName": "[parameters('dnsResourceGroupName')]",
266 | "DnsSettings__AzureServicePrincipal__UseManagendIdentity": "true",
267 | "DnsSettings__AzureSubscription__Tenant": "[subscription().tenantId]",
268 | "DnsSettings__AzureSubscription__SubscriptionId": "[subscription().subscriptionId]",
269 | "DnsSettings__AzureSubscription__AzureRegion": "AzureGlobalCloud",
270 | "AcmeDnsRequest__CsrInfo__Organization": "[parameters('csrOrganisation')]",
271 | "AcmeDnsRequest__RegistrationEmail": "[parameters('acmeRegistrationEmail')]",
272 | "AcmeDnsRequest__AcmeEnvironment__Name": "[parameters('acmeEnvironment')]",
273 | "AcmeDnsRequest__Host": "[parameters('certificateDomain')]",
274 | "AcmeDnsRequest__PFXPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', parameters('vaultName'), variables('pfxPass'))).secretUriWithVersion, ')')]",
275 | "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(concat('microsoft.insights/components/', variables('appInsightsName'))).InstrumentationKey]",
276 | "WEBSITE_RUN_FROM_PACKAGE": "[parameters('runFromPackage')]",
277 | "CertRenewSchedule": "[parameters('certRenewSchedule')]"
278 | }
279 | }
280 | ]
281 | },
282 | {
283 | "type": "Microsoft.Storage/storageAccounts",
284 | "name": "[variables('storageAccountName')]",
285 | "apiVersion": "2015-06-15",
286 | "location": "[resourceGroup().location]",
287 | "properties": {
288 | "accountType": "Standard_LRS"
289 | }
290 | },
291 | {
292 | "apiVersion": "2014-04-01",
293 | "name": "[variables('appInsightsName')]",
294 | "type": "Microsoft.Insights/components",
295 | "location": "[resourceGroup().location]",
296 | "tags": {
297 | "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('functionAppName'))]": "Resource",
298 | "displayName": "AppInsightsComponent"
299 | },
300 | "properties": {
301 | "applicationId": "[variables('appInsightsName')]"
302 | }
303 | },
304 | {
305 | "apiVersion": "2017-05-10",
306 | "name": "nestedTemplate",
307 | "condition": true,
308 | "type": "Microsoft.Resources/deployments",
309 | "resourceGroup": "[parameters('targetWebAppResourceGroupName')]",
310 | "subscriptionId": "[subscription().subscriptionId]",
311 | "dependsOn": [
312 | "[resourceId('Microsoft.Web/sites/', parameters('functionAppName'))]"
313 | ],
314 | "properties": {
315 | "mode": "Incremental",
316 | "template": {
317 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
318 | "contentVersion": "1.0.0.0",
319 | "parameters": {},
320 | "variables": {},
321 | "resources": [
322 | {
323 | "type": "Microsoft.Authorization/roleAssignments",
324 | "apiVersion": "2017-05-01",
325 | "name": "[parameters('roleNameGuid')]",
326 | "properties": {
327 | "roleDefinitionId": "[variables(parameters('builtInRoleType'))]",
328 | "principalId": "[reference(concat('Microsoft.Web/sites/', parameters('functionAppName')), '2018-02-01', 'Full').identity.principalId]",
329 | "scope": "[variables('scope')]"
330 | }
331 | }
332 | ]
333 | },
334 | "parameters": {}
335 | }
336 | },
337 | {
338 | "apiVersion": "2017-05-10",
339 | "name": "nestedTemplateDns",
340 | "condition": true,
341 | "type": "Microsoft.Resources/deployments",
342 | "resourceGroup": "[parameters('dnsResourceGroupName')]",
343 | "subscriptionId": "[subscription().subscriptionId]",
344 | "dependsOn": [
345 | "[resourceId('Microsoft.Web/sites/', parameters('functionAppName'))]"
346 | ],
347 | "properties": {
348 | "mode": "Incremental",
349 | "template": {
350 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
351 | "contentVersion": "1.0.0.0",
352 | "parameters": {},
353 | "variables": {},
354 | "resources": [
355 | {
356 | "type": "Microsoft.Authorization/roleAssignments",
357 | "apiVersion": "2017-05-01",
358 | "name": "[parameters('roleNameDnsGuid')]",
359 | "properties": {
360 | "roleDefinitionId": "[variables(parameters('builtInRoleType'))]",
361 | "principalId": "[reference(concat('Microsoft.Web/sites/', parameters('functionAppName')), '2018-02-01', 'Full').identity.principalId]",
362 | "scope": "[variables('scopeDns')]"
363 | }
364 | }
365 | ]
366 | },
367 | "parameters": {}
368 | }
369 | }
370 | ],
371 | "outputs": {}
372 | }
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.functionapp.renewer.parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "functionAppName": { "value": "azlerunner2" },
6 | "vaultName": { "value": "azlerunner2" },
7 | "hostingPlanName": { "value": "letsencryptrunner" },
8 | "targetWebAppResourceGroupName": { "value": "LetsEncrypt.Wildcard.Function" },
9 | "targetWebAppName": { "value": "sjkpletsencrypt" },
10 | "dnsZoneName": { "value": "ai4bots.com" },
11 | "dnsResourceGroupName": { "value": "dns" },
12 | "csrOrganisation": { "value": "sjkp" },
13 | "acmeRegistrationEmail": { "value": "mail@sjkp.dk" },
14 | "certificateDomain": { "value": "*.ai4bots.com" },
15 | "runFromPackage": { "value": "https://letsencryptazure.blob.core.windows.net/releases/126.zip" }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/bin/Debug/staging/LetsEncrypt.Azure.ResourceGroup/Deploy-AzureResourceGroup.ps1:
--------------------------------------------------------------------------------
1 | #Requires -Version 3.0
2 | #Requires -Module AzureRM.Resources
3 | #Requires -Module Azure.Storage
4 |
5 | Param(
6 | [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation,
7 | [string] $ResourceGroupName = 'LetsEncrypt.Azure.ResourceGroup',
8 | [switch] $UploadArtifacts,
9 | [string] $StorageAccountName,
10 | [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts',
11 | [string] $TemplateFile = 'Templates\azuredeploy.json',
12 | [string] $TemplateParametersFile = 'Templates\azuredeploy.parameters.json',
13 | [string] $ArtifactStagingDirectory = '.',
14 | [string] $DSCSourceFolder = 'DSC'
15 | )
16 |
17 | Import-Module Azure -ErrorAction SilentlyContinue
18 |
19 | try {
20 | [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(" ","_"), "2.9.1")
21 | } catch { }
22 |
23 | Set-StrictMode -Version 3
24 |
25 | $OptionalParameters = New-Object -TypeName Hashtable
26 | $TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile))
27 | $TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile))
28 |
29 | if ($UploadArtifacts) {
30 | # Convert relative paths to absolute paths if needed
31 | $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory))
32 | $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder))
33 |
34 | Set-Variable ArtifactsLocationName '_artifactsLocation' -Option ReadOnly -Force
35 | Set-Variable ArtifactsLocationSasTokenName '_artifactsLocationSasToken' -Option ReadOnly -Force
36 |
37 | $OptionalParameters.Add($ArtifactsLocationName, $null)
38 | $OptionalParameters.Add($ArtifactsLocationSasTokenName, $null)
39 |
40 | # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present
41 | $JsonContent = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json
42 | $JsonParameters = $JsonContent | Get-Member -Type NoteProperty | Where-Object {$_.Name -eq "parameters"}
43 |
44 | if ($JsonParameters -eq $null) {
45 | $JsonParameters = $JsonContent
46 | }
47 | else {
48 | $JsonParameters = $JsonContent.parameters
49 | }
50 |
51 | $JsonParameters | Get-Member -Type NoteProperty | ForEach-Object {
52 | $ParameterValue = $JsonParameters | Select-Object -ExpandProperty $_.Name
53 |
54 | if ($_.Name -eq $ArtifactsLocationName -or $_.Name -eq $ArtifactsLocationSasTokenName) {
55 | $OptionalParameters[$_.Name] = $ParameterValue.value
56 | }
57 | }
58 |
59 | # Create DSC configuration archive
60 | if (Test-Path $DSCSourceFolder) {
61 | $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter "*.ps1" | ForEach-Object -Process {$_.FullName})
62 | foreach ($DSCSourceFilePath in $DSCSourceFilePaths) {
63 | $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + ".zip"
64 | Publish-AzureRmVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose
65 | }
66 | }
67 |
68 | # Create a storage account name if none was provided
69 | if($StorageAccountName -eq "") {
70 | $subscriptionId = ((Get-AzureRmContext).Subscription.SubscriptionId).Replace('-', '').substring(0, 19)
71 | $StorageAccountName = "stage$subscriptionId"
72 | }
73 |
74 | $StorageAccount = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName})
75 |
76 | # Create the storage account if it doesn't already exist
77 | if($StorageAccount -eq $null){
78 | $StorageResourceGroupName = "ARM_Deploy_Staging"
79 | New-AzureRmResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force
80 | $StorageAccount = New-AzureRmStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation"
81 | }
82 |
83 | $StorageAccountContext = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName}).Context
84 |
85 | # Generate the value for artifacts location if it is not provided in the parameter file
86 | $ArtifactsLocation = $OptionalParameters[$ArtifactsLocationName]
87 | if ($ArtifactsLocation -eq $null) {
88 | $ArtifactsLocation = $StorageAccountContext.BlobEndPoint + $StorageContainerName
89 | $OptionalParameters[$ArtifactsLocationName] = $ArtifactsLocation
90 | }
91 |
92 | # Copy files from the local storage staging location to the storage account container
93 | New-AzureStorageContainer -Name $StorageContainerName -Context $StorageAccountContext -Permission Container -ErrorAction SilentlyContinue *>&1
94 |
95 | $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName}
96 | foreach ($SourcePath in $ArtifactFilePaths) {
97 | $BlobName = $SourcePath.Substring($ArtifactStagingDirectory.length + 1)
98 | Set-AzureStorageBlobContent -File $SourcePath -Blob $BlobName -Container $StorageContainerName -Context $StorageAccountContext -Force
99 | }
100 |
101 | # Generate the value for artifacts location SAS token if it is not provided in the parameter file
102 | $ArtifactsLocationSasToken = $OptionalParameters[$ArtifactsLocationSasTokenName]
103 | if ($ArtifactsLocationSasToken -eq $null) {
104 | # Create a SAS token for the storage container - this gives temporary read-only access to the container
105 | $ArtifactsLocationSasToken = New-AzureStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccountContext -Permission r -ExpiryTime (Get-Date).AddHours(4)
106 | $ArtifactsLocationSasToken = ConvertTo-SecureString $ArtifactsLocationSasToken -AsPlainText -Force
107 | $OptionalParameters[$ArtifactsLocationSasTokenName] = $ArtifactsLocationSasToken
108 | }
109 | }
110 |
111 | # Create or update the resource group using the specified template file and template parameters file
112 | New-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force -ErrorAction Stop
113 |
114 | New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) `
115 | -ResourceGroupName $ResourceGroupName `
116 | -TemplateFile $TemplateFile `
117 | -TemplateParameterFile $TemplateParametersFile `
118 | @OptionalParameters `
119 | -Force -Verbose
120 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/bin/Debug/staging/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.azure.core.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "keyVaultName": {
6 | "type": "string",
7 | "metadata": {
8 | "description": "Name of the Vault"
9 | }
10 | },
11 | "tenantId": {
12 | "type": "string",
13 | "metadata": {
14 | "description": "Tenant Id of the subscription. Get using Get-AzureRmSubscription cmdlet or Get Subscription API"
15 | }
16 | },
17 | "objectId": {
18 | "type": "string",
19 | "metadata": {
20 | "description": "Object Id of the AD user. Get using Get-AzureRmADUser or Get-AzureRmADServicePrincipal cmdlets"
21 | }
22 | },
23 | "keysPermissions": {
24 | "type": "array",
25 | "defaultValue": ["all"],
26 | "metadata": {
27 | "description": "Permissions to keys in the vault. Valid values are: all, create, import, update, get, list, delete, backup, restore, encrypt, decrypt, wrapkey, unwrapkey, sign, and verify."
28 | }
29 | },
30 | "secretsPermissions": {
31 | "type": "array",
32 | "defaultValue": ["all"],
33 | "metadata": {
34 | "description": "Permissions to secrets in the vault. Valid values are: all, get, set, list, and delete."
35 | }
36 | },
37 | "skuName": {
38 | "type": "string",
39 | "defaultValue": "Standard",
40 | "allowedValues": [
41 | "Standard",
42 | "Premium"
43 | ],
44 | "metadata": {
45 | "description": "SKU for the vault"
46 | }
47 | },
48 | "enableVaultForDeployment": {
49 | "type": "bool",
50 | "defaultValue": false,
51 | "allowedValues": [
52 | true,
53 | false
54 | ],
55 | "metadata": {
56 | "description": "Specifies if the vault is enabled for a VM deployment"
57 | }
58 | },
59 | "enableVaultForDiskEncryption": {
60 | "type": "bool",
61 | "defaultValue": false,
62 | "allowedValues": [
63 | true,
64 | false
65 | ],
66 | "metadata": {
67 | "description": "Specifies if the azure platform has access to the vault for enabling disk encryption scenarios."
68 | }
69 | },
70 | "enabledForTemplateDeployment": {
71 | "type": "bool",
72 | "defaultValue": false,
73 | "allowedValues": [
74 | true,
75 | false
76 | ],
77 | "metadata": {
78 | "description": "Specifies whether Azure Resource Manager is permitted to retrieve secrets from the key vault."
79 | }
80 | },
81 | "appName": {
82 | "type": "string",
83 | "metadata": {
84 | "description": "The name of the function app that you wish to create."
85 | }
86 | },
87 | "storageAccountType": {
88 | "type": "string",
89 | "defaultValue": "Standard_LRS",
90 | "allowedValues": [
91 | "Standard_LRS",
92 | "Standard_GRS",
93 | "Standard_ZRS",
94 | "Premium_LRS"
95 | ],
96 | "metadata": {
97 | "description": "Storage Account type"
98 | }
99 | }
100 | },
101 | "variables": {
102 | "functionAppName": "[parameters('appName')]",
103 | "hostingPlanName": "[parameters('appName')]",
104 | "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]",
105 | "armResourceProvideServicePrincipalId": "abfa0a7c-a6b6-4736-8310-5855508787cd"
106 | },
107 | "resources": [
108 | {
109 | "type": "Microsoft.KeyVault/vaults",
110 | "name": "[parameters('keyVaultName')]",
111 | "apiVersion": "2015-06-01",
112 | "location": "[resourceGroup().location]",
113 | "properties": {
114 | "enabledForDeployment": "[parameters('enableVaultForDeployment')]",
115 | "enabledForDiskEncryption": "[parameters('enableVaultForDiskEncryption')]",
116 | "enabledForTemplateDeployment": "[parameters('enabledForTemplateDeployment')]",
117 | "tenantId": "[parameters('tenantId')]",
118 | "accessPolicies": [
119 | {
120 | "tenantId": "[parameters('tenantId')]",
121 | "objectId": "[parameters('objectId')]",
122 | "permissions": {
123 | "keys": "[parameters('keysPermissions')]",
124 | "secrets": "[parameters('secretsPermissions')]"
125 | }
126 | },
127 | {
128 | "tenantId": "[parameters('tenantId')]",
129 | "objectId": "[variables('armResourceProvideServicePrincipalId')]",
130 | "permissions": {
131 | "keys": "[parameters('keysPermissions')]",
132 | "secrets": "[parameters('secretsPermissions')]"
133 | }
134 | }
135 | ],
136 | "sku": {
137 | "name": "[parameters('skuName')]",
138 | "family": "A"
139 | }
140 | }
141 | },
142 | {
143 | "type": "Microsoft.Storage/storageAccounts",
144 | "name": "[variables('storageAccountName')]",
145 | "apiVersion": "2015-06-15",
146 | "location": "[resourceGroup().location]",
147 | "properties": {
148 | "accountType": "[parameters('storageAccountType')]"
149 | }
150 | },
151 | {
152 | "type": "Microsoft.Web/serverfarms",
153 | "apiVersion": "2015-04-01",
154 | "name": "[variables('hostingPlanName')]",
155 | "location": "[resourceGroup().location]",
156 | "properties": {
157 | "name": "[variables('hostingPlanName')]",
158 | "computeMode": "Dynamic",
159 | "sku": "Dynamic"
160 | }
161 | },
162 | {
163 | "apiVersion": "2015-08-01",
164 | "type": "Microsoft.Web/sites",
165 | "name": "[variables('functionAppName')]",
166 | "location": "[resourceGroup().location]",
167 | "kind": "functionapp",
168 | "properties": {
169 | "name": "[variables('functionAppName')]",
170 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]"
171 | },
172 | "dependsOn": [
173 | "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
174 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
175 | ],
176 | "resources": [
177 | {
178 | "apiVersion": "2016-03-01",
179 | "name": "appsettings",
180 | "type": "config",
181 | "dependsOn": [
182 | "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]",
183 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
184 | ],
185 | "properties": {
186 | "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]",
187 | "AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2015-05-01-preview').key1,';')]",
188 | "FUNCTIONS_EXTENSION_VERSION": "latest"
189 | }
190 | }
191 | ]
192 | }
193 | ]
194 | }
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.ResourceGroup/bin/Debug/staging/LetsEncrypt.Azure.ResourceGroup/Templates/letsencrypt.azure.core.parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "keyVaultName": {
6 | "value": "letsencrypt-vault"
7 | },
8 | "tenantId": {
9 | "value": ""
10 | },
11 | "objectId": {
12 | "value": ""
13 | },
14 | "keysPermissions": {
15 | "value": [
16 | "all"
17 | ]
18 | },
19 | "secretsPermissions": {
20 | "value": [
21 | "all"
22 | ]
23 | },
24 | "skuName": {
25 | "value": "Standard"
26 | },
27 | "enableVaultForDeployment": {
28 | "value": false
29 | },
30 | "enableVaultForDiskEncryption": {
31 | "value": false
32 | },
33 | "enabledForTemplateDeployment": {
34 | "value": false
35 | },
36 | "appName": {
37 | "value": "letsencryptfunctionapp"
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Runner/LetsEncrypt.Azure.Runner.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp2.1
6 | 7.2
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Runner/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Logging;
3 | using System;
4 | using LetsEncrypt.Azure.Core.V2;
5 | using Microsoft.Extensions.Configuration;
6 | using LetsEncrypt.Azure.Core.V2.Models;
7 | using LetsEncrypt.Azure.Core.V2.DnsProviders;
8 | using LetsEncrypt.Azure.Core;
9 | using System.Threading.Tasks;
10 | using LetsEncrypt.Azure.Core.V2.CertificateStores;
11 | using Microsoft.Azure.KeyVault;
12 | using Microsoft.Rest;
13 |
14 | namespace LetsEncrypt.Azure.Runner
15 | {
16 | class Program
17 | {
18 | static IConfiguration Configuration;
19 | async static Task Main(string[] args)
20 | {
21 | Configuration = new ConfigurationBuilder()
22 | .AddJsonFile("settings.json", true)
23 | .AddEnvironmentVariables()
24 | .Build();
25 |
26 | IServiceCollection serviceCollection = new ServiceCollection();
27 | serviceCollection.AddLogging(c =>
28 | {
29 | c.AddConsole();
30 | //c.AddDebug();
31 | })
32 | .Configure(options => options.MinLevel = LogLevel.Information);
33 |
34 | var azureAppSettings = new AzureWebAppSettings[] { };
35 |
36 | if (Configuration.GetSection("AzureAppService").Exists())
37 | {
38 | azureAppSettings = new[] { Configuration.GetSection("AzureAppService").Get() };
39 | }
40 | if (Configuration.GetSection("AzureAppServices").Exists())
41 | {
42 | azureAppSettings = Configuration.GetSection("AzureAppServices").Get();
43 | }
44 |
45 | if (azureAppSettings.Length == 0)
46 | {
47 | serviceCollection.AddNullCertificateConsumer();
48 | }
49 | else
50 | {
51 | serviceCollection.AddAzureAppService(azureAppSettings);
52 | }
53 |
54 | if (!string.IsNullOrEmpty(Configuration.GetSection("CertificateStore").Get().ConnectionString))
55 | {
56 | var blobSettings = Configuration.GetSection("CertificateStore").Get();
57 | serviceCollection.AddAzureBlobStorageCertificateStore(blobSettings.ConnectionString);
58 | }
59 | else if (Configuration.GetSection("CertificateStore").Get().BaseUrl != null)
60 | {
61 | var keyVaultSettings = Configuration.GetSection("CertificateStore").Get();
62 | serviceCollection.AddTransient((service) =>
63 | {
64 | return new KeyVaultClient(AzureHelper.GetAzureCredentials(keyVaultSettings.AzureServicePrincipal, keyVaultSettings.AzureSubscription), new MessageLoggingHandler(service.GetService()));
65 | });
66 | serviceCollection.AddKeyVaultCertificateStore(keyVaultSettings.BaseUrl);
67 | }
68 | else
69 | {
70 | //Nothing default a null certificate store will be added.
71 | }
72 |
73 |
74 |
75 | if (Configuration.GetSection("DnsSettings").Get().ShopperId != null)
76 | {
77 | serviceCollection.AddAcmeClient(Configuration.GetSection("DnsSettings").Get());
78 | } else if (Configuration.GetSection("DnsSettings").Get().AccountName != null)
79 | {
80 | serviceCollection.AddAcmeClient(Configuration.GetSection("DnsSettings").Get());
81 | } else if (Configuration.GetSection("DnsSettings").Get().ResourceGroupName != null)
82 | {
83 | serviceCollection.AddAcmeClient(Configuration.GetSection("DnsSettings").Get());
84 | }
85 |
86 | serviceCollection.AddTransient();
87 |
88 | var serviceProvider = serviceCollection.BuildServiceProvider();
89 | var dnsRequest = Configuration.GetSection("AcmeDnsRequest").Get();
90 |
91 | var app = serviceProvider.GetService();
92 | await app.Run(dnsRequest, Configuration.GetValue("RenewXNumberOfDaysBeforeExpiration") ?? 22);
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/LetsEncrypt.Azure.Runner/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "newProfile1": {
4 | "commandName": "Project"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------