Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 221a4fcd6e | |||
| 37cfd237cc | |||
| 668f3c3e28 |
@@ -401,6 +401,67 @@ func azure functionapp publish <function-app-name> --no-build
|
|||||||
|
|
||||||
`--no-build` tells `func` to skip its own build step since we already compiled the TypeScript output. To also push application settings from `local.settings.json` in the same step, append `--publish-local-settings --overwrite-settings`. This is useful for the initial deployment or when settings and code change together.
|
`--no-build` tells `func` to skip its own build step since we already compiled the TypeScript output. To also push application settings from `local.settings.json` in the same step, append `--publish-local-settings --overwrite-settings`. This is useful for the initial deployment or when settings and code change together.
|
||||||
|
|
||||||
|
### Triggering and verifying a deployed function
|
||||||
|
|
||||||
|
Set these variables once, then all snippets below are copy-pastable:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
FUNCTION_APP=<function-app-name>
|
||||||
|
RESOURCE_GROUP=<resource-group>
|
||||||
|
APP_INSIGHTS=<app-insights-name> # only needed for the query at the end
|
||||||
|
```
|
||||||
|
|
||||||
|
After deployment, trigger the timer function immediately without waiting for the schedule:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
MASTER_KEY=$(az functionapp keys list \
|
||||||
|
--name $FUNCTION_APP \
|
||||||
|
--resource-group $RESOURCE_GROUP \
|
||||||
|
--query masterKey -o tsv)
|
||||||
|
|
||||||
|
curl -X POST \
|
||||||
|
"https://${FUNCTION_APP}.azurewebsites.net/admin/functions/acmeProvisioner" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-functions-key: ${MASTER_KEY}" \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Stream live logs while the function runs:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
func azure functionapp logstream $FUNCTION_APP
|
||||||
|
```
|
||||||
|
|
||||||
|
Or tail logs via the Azure CLI:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
az webapp log tail \
|
||||||
|
--name $FUNCTION_APP \
|
||||||
|
--resource-group $RESOURCE_GROUP
|
||||||
|
```
|
||||||
|
|
||||||
|
A successful run produces output similar to:
|
||||||
|
|
||||||
|
```
|
||||||
|
Initializing ACME account
|
||||||
|
Scanning DNS zones
|
||||||
|
Found 2 domain(s) tagged for ACME management
|
||||||
|
Issuing certificate for *.example.com, example.com
|
||||||
|
Certificate stored in KeyVault as 'cert-wildcard-example-com'
|
||||||
|
Done. Issued: 1, Renewed: 0, Skipped: 0, Errors: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
If Application Insights is configured, query recent invocations with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
az monitor app-insights query \
|
||||||
|
--app $APP_INSIGHTS \
|
||||||
|
--resource-group $RESOURCE_GROUP \
|
||||||
|
--analytics-query "traces | where timestamp > ago(1h) | order by timestamp desc"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Local testing
|
### Local testing
|
||||||
|
|
||||||
Create `local.settings.json` at the project root (gitignored) and fill in your values:
|
Create `local.settings.json` at the project root (gitignored) and fill in your values:
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/azure-acme-provisioner",
|
"name": "@slawek/azure-acme-provisioner",
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@slawek/azure-acme-provisioner",
|
"name": "@slawek/azure-acme-provisioner",
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-authorization": "^9.0.0",
|
"@azure/arm-authorization": "^9.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@slawek/azure-acme-provisioner",
|
"name": "@slawek/azure-acme-provisioner",
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Sławomir Koszewski",
|
"name": "Sławomir Koszewski",
|
||||||
"url": "https://github.com/skoszewski"
|
"url": "https://github.com/skoszewski"
|
||||||
|
|||||||
+12
-18
@@ -1,6 +1,7 @@
|
|||||||
import { TokenCredential } from '@azure/identity';
|
import { TokenCredential } from '@azure/identity';
|
||||||
import {
|
import {
|
||||||
CertificateClient,
|
CertificateClient,
|
||||||
|
CertificatePolicy,
|
||||||
KeyVaultCertificateWithPolicy,
|
KeyVaultCertificateWithPolicy,
|
||||||
} from '@azure/keyvault-certificates';
|
} from '@azure/keyvault-certificates';
|
||||||
import { SecretClient } from '@azure/keyvault-secrets';
|
import { SecretClient } from '@azure/keyvault-secrets';
|
||||||
@@ -48,24 +49,17 @@ export class KeyVaultStore {
|
|||||||
|
|
||||||
async importCertificate(name: string, cert: string | Buffer, format: 'pem' | 'pfx' = 'pem', password?: string): Promise<void> {
|
async importCertificate(name: string, cert: string | Buffer, format: 'pem' | 'pfx' = 'pem', password?: string): Promise<void> {
|
||||||
const certBuffer = typeof cert === 'string' ? Buffer.from(cert) : cert;
|
const certBuffer = typeof cert === 'string' ? Buffer.from(cert) : cert;
|
||||||
// The high-level CertificateClient spreads `policy` into import params but the
|
const contentType = format === 'pfx' ? 'application/x-pkcs12' : 'application/x-pem-file';
|
||||||
// generated serializer reads `certificatePolicy` — a key mismatch that silently
|
try {
|
||||||
// drops content_type from the REST body. Call the internal client directly so
|
// When a certificate already exists, Azure validates the incoming bytes against
|
||||||
// secret_props.content_type reaches Azure (required for PFX; without it Azure
|
// its stored policy's content_type. Updating the policy first tells Azure to
|
||||||
// defaults to PEM parsing and rejects binary PFX data).
|
// expect the new format; without this, converting PEM→PFX (or vice-versa)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// fails because Azure tries to parse binary PFX data as PEM.
|
||||||
const internalClient = (this.certClient as any).client;
|
await this.certClient.updateCertificatePolicy(name, { contentType } as CertificatePolicy);
|
||||||
await internalClient.importCertificate(name, {
|
} catch {
|
||||||
base64EncodedCertificate: format === 'pem'
|
// Certificate doesn't exist yet — no policy to update, proceed to import.
|
||||||
? certBuffer.toString('ascii')
|
}
|
||||||
: certBuffer.toString('base64'),
|
await this.certClient.importCertificate(name, certBuffer, { password });
|
||||||
password,
|
|
||||||
certificatePolicy: {
|
|
||||||
secretProperties: {
|
|
||||||
contentType: format === 'pfx' ? 'application/x-pkcs12' : 'application/x-pem-file',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -32,9 +32,15 @@ export function pfxToPem(pfxBuffer: Buffer, password = ''): PemBundle {
|
|||||||
if (!keyBag?.key) throw new Error('No private key found in PFX');
|
if (!keyBag?.key) throw new Error('No private key found in PFX');
|
||||||
if (allCertBags.length === 0) throw new Error('No certificates found in PFX');
|
if (allCertBags.length === 0) throw new Error('No certificates found in PFX');
|
||||||
|
|
||||||
|
// Export as PKCS#8 — Azure Key Vault rejects PKCS#1 (BEGIN RSA PRIVATE KEY) in PEM imports
|
||||||
|
const pkcs8Asn1 = forge.pki.wrapRsaPrivateKey(forge.pki.privateKeyToAsn1(keyBag.key));
|
||||||
|
const pkcs8Der = forge.asn1.toDer(pkcs8Asn1).getBytes();
|
||||||
|
const pkcs8B64 = forge.util.encode64(pkcs8Der).match(/.{1,64}/g)!.join('\n');
|
||||||
|
const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${pkcs8B64}\n-----END PRIVATE KEY-----\n`;
|
||||||
|
|
||||||
const [first, ...rest] = allCertBags.map(bag => forge.pki.certificateToPem(bag.cert!));
|
const [first, ...rest] = allCertBags.map(bag => forge.pki.certificateToPem(bag.cert!));
|
||||||
return {
|
return {
|
||||||
privateKeyPem: forge.pki.privateKeyToPem(keyBag.key),
|
privateKeyPem,
|
||||||
certPem: first,
|
certPem: first,
|
||||||
chainPem: rest.join(''),
|
chainPem: rest.join(''),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user