Skip to content

Commit

Permalink
fix: conditional tls setup for prod/nonprod (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevholmes authored Sep 25, 2023
1 parent df2d16b commit 81ae0a5
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 33 deletions.
5 changes: 1 addition & 4 deletions .sops.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
creation_rules:
- azure_kv:
vault_url: https://elyclover-kv.vault.azure.net
name: sops-key
version: 06473759a48f4ed39ef5343909e2e436
- azure_keyvault: https://elyclover-kv.vault.azure.net/keys/sops-key/06473759a48f4ed39ef5343909e2e436
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# elyclover.com-infra

# targets to handle SOPS encryption/decryption using Azure keyvault
decrypt:
@./scripts/sops.sh $@
encrypt:
@./scripts/sops.sh $@
2 changes: 2 additions & 0 deletions Pulumi.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ config:
elyclover.com-infra:keyVaultAzureSubscription: df058eb7-0193-43d2-b1ff-937439336e86
elyclover.com-infra:keyVaultName: elyclover-kv
elyclover.com-infra:keyvaultResourceGroup: root
# cloverstack.dev vs elyclover.com?
elyclover.com-infra:prodCertName: cloverstack-apex
github:token:
secure: AAABALUZicxKxjn0TkjWVU+L79eHIid/l3kt/CNDpISITEQlITsMZAlOIyHae4P77D03rcs0+/7n94yb/5D6I4P8JbA4JL8+HXHx1Yt+hdB6zbs+T5ZL+SyRrvGnKmXNGv3mgZdhH8acVp9oax/eUAjLUrtekqMh+EOXTiI=
3 changes: 3 additions & 0 deletions assets/tls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# pfx storage

This directory is for storing pfx certificates that have been encrypted at-rest with SOPS.
75 changes: 51 additions & 24 deletions cdn.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)

type epCfgs struct {
userManaged legacycdn.EndpointCustomDomainUserManagedHttpsArgs
cdnManaged legacycdn.EndpointCustomDomainCdnManagedHttpsArgs
domainArgs legacycdn.EndpointCustomDomainArgs
}

func createCdnProfile(ctx *pulumi.Context, cdnName string, azureRg pulumi.StringOutput) (profile *nativecdn.Profile, err error) {
var cdnProfileArgs = nativecdn.ProfileArgs{
Location: pulumi.String("global"),
Expand Down Expand Up @@ -95,32 +101,54 @@ func createCdnEndpoint(ctx *pulumi.Context, epName string, cdnProfile *nativecdn
return
}

func newEndpointCustomDomain(ctx *pulumi.Context, epdName string, endpoint *nativecdn.Endpoint, domain pulumi.StringOutput) (epd *legacycdn.EndpointCustomDomain, err error) {
func newEndpointCustomDomain(ctx *pulumi.Context, epdName string, endpoint *nativecdn.Endpoint, domain pulumi.StringOutput,
cfg *config.Config) (epd *legacycdn.EndpointCustomDomain, err error) {
// Utilize the azure legacy provider since it supports setting up auto-TLS for CDN custom domains
// azure-native provider strangely lacks support for CDN-managed TLS on custom domains... pushing front door? $$$
cdnManagedHttps := legacycdn.EndpointCustomDomainCdnManagedHttpsArgs{
CertificateType: pulumi.String("Dedicated"),
ProtocolType: pulumi.String("ServerNameIndication"),
TlsVersion: pulumi.String("TLS12"),
// azure-native provider strangely lacks support for CDN-managed TLS on custom domains
epCfg := epCfgs{
userManaged: legacycdn.EndpointCustomDomainUserManagedHttpsArgs{
TlsVersion: pulumi.String("TLS12"),
},
cdnManaged: legacycdn.EndpointCustomDomainCdnManagedHttpsArgs{
CertificateType: pulumi.String("Dedicated"),
ProtocolType: pulumi.String("ServerNameIndication"),
TlsVersion: pulumi.String("TLS12"),
},
domainArgs: legacycdn.EndpointCustomDomainArgs{
CdnEndpointId: endpoint.ID(),
HostName: domain,
// set user managed or cdn managed https based on stack (env)
},
}
endpointCustomDomainArgs := legacycdn.EndpointCustomDomainArgs{
CdnEndpointId: endpoint.ID(),
HostName: domain,
CdnManagedHttps: &cdnManagedHttps,
switch ctx.Stack() {
// user managed endpoint https config (byo cert)
case "prod":
cert, err := getSecretByName(ctx, cfg.Require("keyVaultName"), cfg.Require("keyvaultResourceGroup"),
cfg.Require("prodCertName"))
if err != nil {
return epd, err
}
epCfg.userManaged.KeyVaultSecretId = pulumi.String(cert.Properties.SecretUri)
epCfg.domainArgs.UserManagedHttps = epCfg.userManaged
// azure cdn managed endpoint https config (auto-gen/rotated cert)
default:
epCfg.domainArgs.CdnManagedHttps = epCfg.cdnManaged
}
epd, err = legacycdn.NewEndpointCustomDomain(ctx, epdName, &endpointCustomDomainArgs)
epd, err = legacycdn.NewEndpointCustomDomain(ctx, epdName, &epCfg.domainArgs)
if err != nil {
fmt.Println("ERROR: creating custom domain for CDN endpoint failed")
return epd, err
}

return
}

// Non-prod environments use sub-domains (eg dev.tld.com) and Azure CDN (classic) will auto-generate and set up the TLS for us
// Non-prod environments use sub-domains (eg dev.tld.com) and Azure CDN (classic) will auto-generate and set up the TLS for us.
// Production uses an apex domain (eg tld.com) which Azure doesn't support free TLS certs + rotation on (:shrug:)
// so we'll need to set up a Service Principal registered under the Azure CDN App profile and give it RBAC access to
// an external Azure KeyVault resource that contains our pfx certificate needed for the prod tld.com domain.
func setupTlsTermination(ctx *pulumi.Context, cfg *config.Config, ep *nativecdn.Endpoint, fqdn pulumi.StringOutput) (err error) {
switch ctx.Stack() {
case "prod":
if ctx.Stack() == "prod" {
// Register Azure CDN Application as Service Principal in AD/Entra tenant so it can fetch TLS pfx data in external Keystore
cdnId := cfg.Require("Microsoft.AzureFrontDoor-Cdn")
nsp, err := azuread.NewServicePrincipal(ctx, cdnId, &azuread.ServicePrincipalArgs{
Expand All @@ -131,11 +159,11 @@ func setupTlsTermination(ctx *pulumi.Context, cfg *config.Config, ep *nativecdn.
if err != nil {
return err
}
keyVaultScope := pulumi.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/vaults/%s",
cfg.Require("keyVaultAzureSubscription"), cfg.Require("keyvaultResourceGroup"), cfg.Require("keyVaultName"))
// assign predefined "Key Vault Secret User" RoleDefinitionId to Service Principal we just created
// https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#key-vault-secrets-user
// This allows Azure CDN to access the pfx keys we purchase/generate external to pulumi (until ACME support is native)
// This allows Azure CDN to access the pfx keys we purchase/generate external to pulumi (quite a bit cheaper than asking Azure to do it.)
keyVaultScope := pulumi.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/vaults/%s",
cfg.Require("keyVaultAzureSubscription"), cfg.Require("keyvaultResourceGroup"), cfg.Require("keyVaultName"))
_, err = authorization.NewRoleAssignment(ctx, "AzureFDCDNreadKVCerts", &authorization.RoleAssignmentArgs{
PrincipalId: nsp.ID(),
PrincipalType: pulumi.String("ServicePrincipal"),
Expand All @@ -145,13 +173,12 @@ func setupTlsTermination(ctx *pulumi.Context, cfg *config.Config, ep *nativecdn.
if err != nil {
return err
}
default:
// newEndpointCustomDomain() will need to be refactored to support conditional for auto-tls setup
// Add Custom Domain to CDN to set up automatic TLS termination/cert rotation
_, err = newEndpointCustomDomain(ctx, cfg.Require("siteKey")+ctx.Stack(), ep, fqdn)
if err != nil {
return err
}
}

// Add Custom Domain to CDN for prod or non-prod
_, err = newEndpointCustomDomain(ctx, cfg.Require("siteKey")+ctx.Stack(), ep, fqdn, cfg)
if err != nil {
return err
}

return
Expand Down
13 changes: 11 additions & 2 deletions dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@ func createDnsRecordByEnv(ctx *pulumi.Context, dnsRG string, dz *dns.LookupZoneR
if err != nil {
return d, err
}
// strip out trailing '.' from CNAME's returned FQDN string within Azure DNS API
// create CNAME 'cdnverify.tld.com' record
cdnVerify := "cdnverify"
cdnVerifyHostname := ep.HostName.ApplyT(func(h string) (r string) {
r = cdnVerify + "." + h
return
}).(pulumi.StringOutput)
_, err = createCNAMERecordPointingAtCdnEndpoint(ctx, dnsRG, dz, cdnVerifyHostname, cdnVerify, siteKey)
if err != nil {
return d, err
}
// strip out trailing '.' from A record returned FQDN string within Azure DNS API
d = dnsRecord.Fqdn.ApplyT(func(fqdn string) (string, error) {
h, found := strings.CutSuffix(fqdn, ".")
if !found {
Expand Down Expand Up @@ -77,7 +87,6 @@ func createCNAMERecordPointingAtCdnEndpoint(ctx *pulumi.Context, dnsRG string, d

func createARecordPointingAtCdnResourceID(ctx *pulumi.Context, dnsRG string, dz *dns.LookupZoneResult, tg pulumi.StringOutput, envKey string, siteKey string) (record *dns.ARecord, err error) {
dnsRecordArgs := dns.ARecordArgs{
//Name: pulumi.String(envKey),
Name: pulumi.String("@"),
ZoneName: pulumi.String(dz.Name),
ResourceGroupName: pulumi.String(dnsRG),
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21
require (
github.com/pulumi/pulumi-azure-native-sdk/authorization/v2 v2.8.0
github.com/pulumi/pulumi-azure-native-sdk/cdn/v2 v2.6.0
github.com/pulumi/pulumi-azure-native-sdk/keyvault/v2 v2.8.0
github.com/pulumi/pulumi-azure-native-sdk/resources/v2 v2.6.0
github.com/pulumi/pulumi-azure-native-sdk/storage/v2 v2.6.0
github.com/pulumi/pulumi-azure/sdk/v5 v5.49.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ github.com/pulumi/pulumi-azure-native-sdk/authorization/v2 v2.8.0 h1:xbTAISsowIK
github.com/pulumi/pulumi-azure-native-sdk/authorization/v2 v2.8.0/go.mod h1:R+oOw7l+XFwvli6yGhNQutwpOD7i97Z+yg2D0SkA4+U=
github.com/pulumi/pulumi-azure-native-sdk/cdn/v2 v2.6.0 h1:ZhqRnPjcAjtitow78ZJBOPEoUApc7AkHg5Hz6EN6LlQ=
github.com/pulumi/pulumi-azure-native-sdk/cdn/v2 v2.6.0/go.mod h1:Zjmi/vd0b+sCj8fifXiUK701lGnhia6FK5Nl67wFJhA=
github.com/pulumi/pulumi-azure-native-sdk/keyvault/v2 v2.8.0 h1:wwVp83pveI4fLh2KHiAD/xSmqPkwrYN2VY8DeWWYPL4=
github.com/pulumi/pulumi-azure-native-sdk/keyvault/v2 v2.8.0/go.mod h1:GyiEY70atSOSUnipRvoBnkt6M/idXgpq+28b8Uug1Ok=
github.com/pulumi/pulumi-azure-native-sdk/resources/v2 v2.6.0 h1:TyDRtmPZlEsaEMjkHpNggohZyLup8gyr8dMx2fId+4A=
github.com/pulumi/pulumi-azure-native-sdk/resources/v2 v2.6.0/go.mod h1:n25zeuzMVh+zBxgp7ed1yNlOu/h054/Xv47J+/l6Ejk=
github.com/pulumi/pulumi-azure-native-sdk/storage/v2 v2.6.0 h1:VwJOP3VNejnpylsWJtuLUNHK7TwzRBlkBp3pxcgA1CE=
Expand Down
4 changes: 2 additions & 2 deletions iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func generateCICDServicePrincipal(ctx *pulumi.Context, sa *storage.StorageAccoun

// authorize new SP to modify any resources required to deploy code to this project
ra := []RoleAssignments{
{cicd + "-storagerole", StorageContributor, ep.ID()},
{cicd + "-cdnrole", CdnContributor, sa.ID()},
{cicd + "-storagerole", StorageContributor, sa.ID()},
{cicd + "-cdnrole", CdnContributor, ep.ID()},
}
for _, v := range ra {
_, err = authorization.NewRoleAssignment(ctx, v.Name, &authorization.RoleAssignmentArgs{
Expand Down
20 changes: 20 additions & 0 deletions keyvault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import (
//legacykeyvault "github.com/pulumi/pulumi-azure/sdk/v5/go/azure/keyvault"
"github.com/pulumi/pulumi-azure-native-sdk/keyvault/v2"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func getSecretByName(ctx *pulumi.Context, kv string, rg string, name string) (secret *keyvault.LookupSecretResult, err error) {
secret, err = keyvault.LookupSecret(ctx, &keyvault.LookupSecretArgs{
VaultName: kv,
SecretName: name,
ResourceGroupName: rg,
})
if err != nil {
return secret, err
}

return
}
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func main() {
return err
}

// Set up TLS depending on environment
// Set up TLS depending on environment and custom domain types
err = setupTlsTermination(ctx, cfg, endpoint, fqdn)
if err != nil {
return err
Expand Down
24 changes: 24 additions & 0 deletions scripts/sops.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash

TLS_FILE_LOCATION=./assets/tls
FILES=("$TLS_FILE_LOCATION"/*)

if [ -z "$1" ]; then
echo "requires either 'encrypt or 'decrypt' argument e.g.: ./sops.sh encrypt"
fi

if [ "$1" == "encrypt" ]; then
for f in "${FILES[@]}"
do
echo encrypting "$f"
sops --encrypt --in-place "$f"
done
elif [ "$1" == "decrypt" ]; then
for f in "${FILES[@]}"
do
echo decrypting "$f"
sops --decrypt --in-place "$f"
done
else
echo "FATAL: unknown argument: $1"
fi

0 comments on commit 81ae0a5

Please sign in to comment.