├── .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 | [![Build status](https://dev.azure.com/letsencrypt/letsencrypt/_apis/build/status/LetsEncrypt-Azure-CI)](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 | ![Overview of infrastructure](media/letsencrypt-azure-overiew.png) 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 | } --------------------------------------------------------------------------------