From d433569bab7d1ffcdc2caf9a72f8c46f8c97956b Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Fri, 22 May 2026 12:11:58 +0200 Subject: [PATCH] feat: add assign-role command to manage Key Vault roles for domain certificates --- README.md | 3 ++- package-lock.json | 21 +++++++++++++++++++-- package.json | 3 ++- src/cli.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a2fbc99..41cb859 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ Commands: scan List all domains tagged for ACME management status Show certificate expiry status for all managed domains renew Force-renew a certificate for a specific domain - download Download the PEM bundle for a domain from Key Vault + download Download the PEM bundle for a domain from Key Vault + assign-role Assign Key Vault roles to a principal for a domain certificate Common options: --keyvault-url Azure KeyVault URL diff --git a/package-lock.json b/package-lock.json index 2dea93a..df779eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "azure-acme-provisioner", - "version": "0.3.4", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "azure-acme-provisioner", - "version": "0.3.4", + "version": "0.4.0", "license": "MIT", "dependencies": { + "@azure/arm-authorization": "^9.0.0", "@azure/arm-dns": "^5.1.0", "@azure/functions": "^4.14.0", "@azure/identity": "^4.13.1", @@ -73,6 +74,22 @@ "node": ">=12.0.0" } }, + "node_modules/@azure/arm-authorization": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@azure/arm-authorization/-/arm-authorization-9.0.0.tgz", + "integrity": "sha512-GdiCA8IA1gO+qcCbFEPj+iLC4+3ByjfKzmeAnkP7MdlL84Yo30Huo/EwbZzwRjYybXYUBuFxGPBB+yeTT4Ebxg==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.7.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@azure/arm-dns": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@azure/arm-dns/-/arm-dns-5.1.0.tgz", diff --git a/package.json b/package.json index 3953264..f570c8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "azure-acme-provisioner", - "version": "0.3.4", + "version": "0.4.0", "author": { "name": "Sławomir Koszewski", "url": "https://github.com/skoszewski" @@ -37,6 +37,7 @@ "start:function": "func start" }, "dependencies": { + "@azure/arm-authorization": "^9.0.0", "@azure/arm-dns": "^5.1.0", "@azure/functions": "^4.14.0", "@azure/identity": "^4.13.1", diff --git a/src/cli.ts b/src/cli.ts index 55b3fa7..91fef44 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,9 +1,17 @@ #!/usr/bin/env node +import { randomUUID } from 'node:crypto'; import { writeFileSync } from 'node:fs'; +import { AuthorizationManagementClient } from '@azure/arm-authorization'; +import { DefaultAzureCredential } from '@azure/identity'; import { Command } from 'commander'; import { loadConfig } from './lib/config.js'; import { domainToCertName, Provisioner } from './lib/provisioner.js'; +const ROLE_IDS = { + 'Key Vault Certificate User': 'db79e9a7-68ee-4b58-9aeb-b90e7c24fcba', + 'Key Vault Secrets User': '4633458b-17de-408a-b874-0445c86b69e6', +} as const; + const program = new Command(); program @@ -31,6 +39,7 @@ function applyOverrides(options: Record): void { const sharedOptions = (cmd: Command): Command => cmd .option('--keyvault-url ', 'Azure KeyVault URL') + .option('--keyvault-resource-group ', 'Resource group containing the Key Vault') .option('--subscription-id ', 'Azure subscription ID') .option('--resource-group ', 'Resource group to scan (repeatable)', collect, []) .option('--dns-zone ', 'Restrict to specific DNS zone (repeatable)', collect, []) @@ -120,6 +129,39 @@ sharedOptions( if (result.errors.length > 0) process.exit(1); }); +sharedOptions( + program + .command('assign-role ') + .description('Assign Key Vault Certificate User and Secrets User roles to a principal for a domain certificate') + .requiredOption('--principal-id ', 'Azure principal ID to assign roles to') +).action(async (domain: string, options: Record) => { + applyOverrides(options); + const config = loadConfig(); + if (!config.subscriptionId) throw new Error('--subscription-id is required'); + if (!config.keyVaultUrl) throw new Error('--keyvault-url is required'); + const kvRg = options['keyvaultResourceGroup']; + if (!kvRg) throw new Error('--keyvault-resource-group is required'); + + const sub = config.subscriptionId; + const principalId = String(options['principalId']); + const vaultName = new URL(config.keyVaultUrl).hostname.split('.')[0]; + const certName = domainToCertName(domain); + const vaultBase = `/subscriptions/${sub}/resourceGroups/${kvRg}/providers/Microsoft.KeyVault/vaults/${vaultName}`; + const credential = new DefaultAzureCredential(); + const authClient = new AuthorizationManagementClient(credential, sub); + + const assignments = [ + { role: 'Key Vault Certificate User' as const, scope: `${vaultBase}/certificates/${certName}` }, + { role: 'Key Vault Secrets User' as const, scope: `${vaultBase}/secrets/${certName}` }, + ]; + + for (const { role, scope } of assignments) { + const roleDefinitionId = `/subscriptions/${sub}/providers/Microsoft.Authorization/roleDefinitions/${ROLE_IDS[role]}`; + await authClient.roleAssignments.create(scope, randomUUID(), { roleDefinitionId, principalId }); + console.log(`Assigned '${role}' to ${principalId} on ${scope}`); + } +}); + sharedOptions( program .command('download ')