Skip to content

Commit

Permalink
use keyvault name instead of id and allow to have separate keys for w…
Browse files Browse the repository at this point in the history
…eb an pubsub (#8)
  • Loading branch information
jmservera authored Jul 26, 2024
1 parent 1e52c90 commit a4b9ecd
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 53 deletions.
1 change: 1 addition & 0 deletions ocpp-server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,4 @@ FodyWeavers.xsd
*.sln.iml

*parameters.json
*parameters.bicepparam
20 changes: 13 additions & 7 deletions ocpp-server/Makefile
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
THIS_FILE := $(lastword $(MAKEFILE_LIST))
RG_NAME := OCPP
RG_LOCATION := switzerlandnorth
BICEP_PARAMS := main.parameters.bicepparam
WEBAPP_NAME := $(shell az webapp list -g $(RG_NAME) --query "[0].name" -o tsv)
STORAGE_NAME := $(shell az storage account list -g $(RG_NAME) --query "[?starts_with(name,'webdeploy')].name" -o tsv)
EXPIRY := $(shell date -u -d "15 minutes" '+%Y-%m-%dT%H:%MZ')
TEST_SERVER := wss.jmservera.online

preparecli:
az upgrade
az extension add --name webpubsub
az bicep upgrade
npm install -g @azure/web-pubsub-tunnel-tool
@echo "Tools prepared, now you can start using the makefile"
build: $(wildcard api/**/*.cs)
@echo "Building"
build: build-api build-node
build-node: $(wildcard client/*.js)
@echo "Building node client"
cd client && npm install
build-api: $(wildcard api/**/*.cs)
@echo "Building api"
dotnet build api/api.sln
test:
@echo "Testing"
dotnet test api/api.sln
test-client:
@echo "Testing a simple node client"
node client/index.js wss://wss.jmservera.online station2 goodpwd
node client/index.js wss://$(TEST_SERVER) station2 goodpwd
test-client-badauth:
@echo "Testing a simple node client"
node client/index.js wss://wss.jmservera.online station1 badpwd
node client/index.js wss://$(TEST_SERVER) station1 badpwd
clean:
@echo "Cleaning"
dotnet clean api/api.sln
Expand All @@ -41,10 +47,10 @@ start-tunnel:
awps-tunnel run --hub OcppService -c "$$WebPubSubConnectionString" -s $$SubId -g $(RG_NAME) --upstream http://localhost:5110
stop-tunnel:
ps axf | grep awps-tunnel | grep -v grep | awk '{print "kill -9 " $$1}' | sh
infra: $(wildcard infra/**/*.bicep) $(wildcard infra/**/*.parameters.json)
infra: $(wildcard infra/**/*.bicep) $(wildcard infra/*.parameters.bicepparam)
@echo "Deploying infra to Azure"
az group create -n $(RG_NAME) -l $(RG_LOCATION)
az deployment group create -g $(RG_NAME) --template-file infra/main.bicep --parameters infra/main.parameters.json
az deployment group create -g $(RG_NAME) --template-file infra/main.bicep --parameters infra/$(BICEP_PARAMS)
@echo "Setting up user secrets"
CONNECTION_STRING='$(shell az webpubsub list -g $(RG_NAME) --query "[0].name" -o tsv | az webpubsub key show -g $(RG_NAME) -n @- --query "primaryConnectionString" -o tsv)'; \
dotnet user-secrets set 'WEBPUBSUB_SERVICE_CONNECTION_STRING' "$$CONNECTION_STRING" --project api/OcppServer/OcppServer.csproj
Expand All @@ -67,4 +73,4 @@ deploy:
@echo "Waiting for infra to be ready"
sleep 60
@$(MAKE) -f $(THIS_FILE) publish
.PHONY: test clean watch start secrets test-client
.PHONY: test clean watch start secrets test-client infra
87 changes: 61 additions & 26 deletions ocpp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,23 @@ You need:
* An **Azure KeyVault** with a wildcard certificate for the domain you will assign to the Application Gateway. The App Gateway uses it to publish the Web PubSub endpoint and the Web App.
* A **User Assigned Managed Identity** with read access to the KeyVault where the SSL certificate is stored.
* A **Public DNS Zone in Azure**, where the script will create or update the A records for the Web PubSub service. Your user needs modify permissions to these DNS records, and they will be created with a resource reference.
* A **main.parameters.json** file in the root of the project with the following content:

```json
{
"parameters": {
"keyVaultSecretId": {
"value": "<Keyvault Secret Identifier (sid) for the SSL Certificate, you can omit the version number to get always the latest one>"
},
"keyVaultIdentityName": {
"value": "<NAME OF THE Managed Identity that has cert read access rights in the KeyVault>"
},
"keyVaultIdentityRG": {
"value": "<RESOURCE GROUP OF THE MANAGED IDENTITY>"
},
"customDnsZoneName": {
"value": "<BASE DOMAIN NAME FOR THE PUBSUB SERVICE IN THE APPP GATEWAY, EX: mydomain.com>"
},
"pubsubARecordName": {
"value": "<SUBDOMAIN NAME USED FOR THE WEB PUBSUB SERVICE, EX: wss (for wss.mydomain.com)>"
},
"dnsZoneRG": {
"value": "<NAME OF THE RG WHERE THE DNS SERVICE>"
}
}
}
```
* A **main.parameters.bicepparam** file in the root of the project with the following content:

```bicep
using 'main.bicep'
param pubsubKeyVaultCertName = '<KeyVault name of the SSL Certificate for the pub sub service>'
param webKeyVaultCertName ='<KeyVault name of the SSL Certificate for the web test service, can be the same cert if you have a wildcard one>'
param keyVaultName = '<Name of the KeyVault>'
param keyVaultIdentityRG ='<NAME OF THE Managed Identity that has cert read access rights in the KeyVault>'
param customDnsZoneName ='<RESOURCE GROUP OF THE MANAGED IDENTITY>'
param pubsubARecordName ='<SUBDOMAIN NAME USED FOR THE WEB PUBSUB SERVICE, EX: wss (for wss.mydomain.com)>'
param dnsZoneRG ='<NAME OF THE RG WHERE THE DNS SERVICE>'
```

## (Optional) Configure Let's encrypt with your KeyVault and Azure DNS

If you don't have a certificate, you can use the Let's Encrypt service to generate a free certificate for your domain. [This amazing project](https://github.com/shibayan/keyvault-acmebot/wiki/Getting-Started) automates the process of generating the certificate and merging it into your KeyVault. Just follow the instructions, configure the permissions and create a certificate for your domain.

## Project Structure

Expand All @@ -60,3 +51,47 @@ This makefile recipe deploys the infra into your Azure subscription, compiles th
The App Service and Web Pub Sub endpoints are protected with Private Endpoints, and published through an Application Gateway.

![Infra](./img/Architecture.svg)

## FAQ

## The client fails with UNABLE_TO_VERIFY_LEAF_SIGNATURE

If you merged your certificate in KeyVault using a PEM or PFX format, you may need to create the CSR again, generate a new certificate and merge it in p7b format. Take a look to this FAQ section: [Create and merge a certificate signing request in Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/certificates/create-certificate-signing-request?tabs=azure-portal#faqs)

After renewing the certificate, you can deploy the infra again with the new certificate, or reconfigure the App Gateway to use the new certificate in the **Listeners TLS Certificates** section.

Example error in the nodejs client:

```js
node:events:495
throw er; // Unhandled 'error' event
^

Error: unable to verify the first certificate
at TLSSocket.onConnectSecure (node:_tls_wrap:1659:34)
at TLSSocket.emit (node:events:517:28)
at TLSSocket._finishInit (node:_tls_wrap:1070:8)
at ssl.onhandshakedone (node:_tls_wrap:856:12)
Emitted 'error' event on WebSocket instance at:
at emitErrorAndClose (/home/jmservera/source/miscdemos/ocpp-server/client/node_modules/ws/lib/websocket.js:1041:13)
at ClientRequest.<anonymous> (/home/jmservera/source/miscdemos/ocpp-server/client/node_modules/ws/lib/websocket.js:881:5)
at ClientRequest.emit (node:events:517:28)
at TLSSocket.socketErrorListener (node:_http_client:501:9)
at TLSSocket.emit (node:events:517:28)
at emitErrorNT (node:internal/streams/destroy:151:8)
at emitErrorCloseNT (node:internal/streams/destroy:116:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
}
```

Example error with curl:

```bash
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
```
22 changes: 14 additions & 8 deletions ocpp-server/infra/main.bicep
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
@secure()
@description('Secret ID for the TLS Certificate stored in Key Vault')
param keyVaultSecretId string
@description('Name of the TLS Certificate stored in Key Vault for the pubsub service public endpoint')
param pubsubKeyVaultCertName string
@description('Name of the TLS Certificate stored in Key Vault for the web service public endpoint')
param webKeyVaultCertName string
@description('Name of the Key Vault where the certificates are stored')
param keyVaultName string
@description('User Assigned Managed Identity name with Get permissions fo the Key Vault Certificate')
param keyVaultIdentityName string
@description('Resource Group name where the User Assigned Managed Identity was created')
param keyVaultIdentityRG string = resourceGroup().name
@description('Custom DNS Zone Name used for publishing the Web PubSub service endpoint securely')
param customDnsZoneName string = 'jmservera.online'
param customDnsZoneName string
@description('A Record Name for the Web PubSub service endpoint, used as prefix of the DnsZoneName')
param pubsubARecordName string = 'wss'
@description('A Record Name for the Web service endpoint, used as prefix of the DnsZoneName')
param webARecordName string = 'www'
@description('Resource Group name where the DNS Zone was created')
param dnsZoneRG string = 'domainnames'
param dnsZoneRG string
@description('The name for your new Web PubSub Hub. It should be the same name than the class implementing it in the asp.net core project')
param pubSubHubName string = 'OcppService'

Expand All @@ -39,7 +43,7 @@ module hub './modules/webPubSubHub.bicep' = {
name: 'webPubSubHub'
params: {
serviceName: webPubSub.outputs.serviceName
hubName: 'OcppService'
hubName: pubSubHubName
webAppName: webApp.outputs.webSiteName
}
}
Expand Down Expand Up @@ -87,9 +91,11 @@ module appGw './modules/appgw.bicep' = {
location: resourceGroup().location
pubSubServiceName: webPubSub.outputs.serviceName
gwSubnetId: virtualNetwork.outputs.gwSubnetId
keyVaultSecretId: keyVaultSecretId
webKeyVaultCertName: webKeyVaultCertName
pubsubKeyVaultCertName: pubsubKeyVaultCertName
webHostName: webHostName
pubsubHostName: pubsubHostName
keyVaultName: keyVaultName
keyVaultIdentityName: keyVaultIdentityName
keyVaultIdentityRG: keyVaultIdentityRG
webServiceName: webApp.outputs.webSiteName
Expand All @@ -98,7 +104,7 @@ module appGw './modules/appgw.bicep' = {
}

// update A record with appGW public IP
module wssdns './modules/dns.bicep' = {
module wssdns './modules/dns.bicep' = if (customDnsZoneName != '') {
name: 'dnsServicePubSub'
scope: resourceGroup(dnsZoneRG)
params: {
Expand All @@ -108,7 +114,7 @@ module wssdns './modules/dns.bicep' = {
}
}

module wwwdns './modules/dns.bicep' = {
module wwwdns './modules/dns.bicep' = if (customDnsZoneName != '') {
name: 'dnsServiceWeb'
scope: resourceGroup(dnsZoneRG)
params: {
Expand Down
9 changes: 9 additions & 0 deletions ocpp-server/infra/main.parameters.bicepparam.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using 'main.bicep'

param pubsubKeyVaultCertName = '<KeyVault name of the SSL Certificate for the pub sub service>'
param webKeyVaultCertName ='<KeyVault name of the SSL Certificate for the web test service, can be the same cert if you have a wildcard one>'
param keyVaultName = '<Name of the KeyVault>'
param keyVaultIdentityRG ='<NAME OF THE Managed Identity that has cert read access rights in the KeyVault>'
param customDnsZoneName ='<RESOURCE GROUP OF THE MANAGED IDENTITY>'
param pubsubARecordName ='<SUBDOMAIN NAME USED FOR THE WEB PUBSUB SERVICE, EX: wss (for wss.mydomain.com)>'
param dnsZoneRG ='<NAME OF THE RG WHERE THE DNS SERVICE>'
64 changes: 52 additions & 12 deletions ocpp-server/infra/modules/appgw.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ param skuSize string = 'Standard_v2'
param skuTier string = 'Standard_v2'
param skuCapacity int = 1
@secure()
param keyVaultSecretId string
param pubsubHostName string
param webHostName string
param keyVaultName string
param keyVaultIdentityName string
param keyVaultIdentityRG string
param webServiceName string
param pubsubHubName string
param pubsubHubName string
param webKeyVaultCertName string
param pubsubKeyVaultCertName string

var ocppRuleSetName = 'ocppRuleSet'
var pubsubBackendPoolName = 'pubsubBackend'
Expand All @@ -27,6 +29,25 @@ var pubsubListenerName = 'pubsubListener'
var webBackendPoolName = 'webBackend'
var webListenerName = 'webListener'
var webBackendSettingsName = 'webBackendSettings'
var webtls = 'webtls'
var pubsubtls = 'pubsubtls'

var isWildcard = (webKeyVaultCertName == pubsubKeyVaultCertName)

resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
name: keyVaultName
scope: resourceGroup(keyVaultIdentityRG)
}

resource pubsubKeyVaultCertificate 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' existing = {
name: pubsubKeyVaultCertName
parent: keyVault
}

resource webKeyVaultCertificate 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' existing = {
name: webKeyVaultCertName
parent: keyVault
}

resource webPubSub 'Microsoft.SignalRService/webPubSub@2021-10-01' existing = {
name: pubSubServiceName
Expand Down Expand Up @@ -167,14 +188,29 @@ resource appGw 'Microsoft.Network/applicationGateways@2023-02-01' = {
}
}
]
sslCertificates: [
{
name: 'pubsubtls'
properties: {
keyVaultSecretId: keyVaultSecretId
}
}
]
sslCertificates: isWildcard
? [
{
name: pubsubtls
properties: {
keyVaultSecretId: pubsubKeyVaultCertificate.properties.secretUri
}
}
]
: [
{
name: pubsubtls
properties: {
keyVaultSecretId: pubsubKeyVaultCertificate.properties.secretUri
}
}
{
name: webtls
properties: {
keyVaultSecretId: webKeyVaultCertificate.properties.secretUri
}
}
]
httpListeners: [
{
name: pubsubListenerName
Expand All @@ -191,7 +227,7 @@ resource appGw 'Microsoft.Network/applicationGateways@2023-02-01' = {
}
protocol: 'Https'
sslCertificate: {
id: resourceId('Microsoft.Network/applicationGateways/sslCertificates', appgwName, 'pubsubtls')
id: resourceId('Microsoft.Network/applicationGateways/sslCertificates', appgwName, pubsubtls)
}
hostName: pubsubHostName
customErrorConfigurations: []
Expand All @@ -213,7 +249,11 @@ resource appGw 'Microsoft.Network/applicationGateways@2023-02-01' = {
}
protocol: 'Https'
sslCertificate: {
id: resourceId('Microsoft.Network/applicationGateways/sslCertificates', appgwName, 'pubsubtls')
id: resourceId(
'Microsoft.Network/applicationGateways/sslCertificates',
appgwName,
isWildcard ? pubsubtls : webtls
)
}
hostName: webHostName
customErrorConfigurations: []
Expand Down

0 comments on commit a4b9ecd

Please sign in to comment.