feat: add assign-role command to manage Key Vault roles for domain certificates

This commit is contained in:
2026-05-22 12:11:58 +02:00
parent 40ec41da28
commit d433569bab
4 changed files with 65 additions and 4 deletions
+2 -1
View File
@@ -44,7 +44,8 @@ Commands:
scan List all domains tagged for ACME management scan List all domains tagged for ACME management
status Show certificate expiry status for all managed domains status Show certificate expiry status for all managed domains
renew Force-renew a certificate for a specific domain 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: Common options:
--keyvault-url <url> Azure KeyVault URL --keyvault-url <url> Azure KeyVault URL
+19 -2
View File
@@ -1,14 +1,15 @@
{ {
"name": "azure-acme-provisioner", "name": "azure-acme-provisioner",
"version": "0.3.4", "version": "0.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "azure-acme-provisioner", "name": "azure-acme-provisioner",
"version": "0.3.4", "version": "0.4.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@azure/arm-authorization": "^9.0.0",
"@azure/arm-dns": "^5.1.0", "@azure/arm-dns": "^5.1.0",
"@azure/functions": "^4.14.0", "@azure/functions": "^4.14.0",
"@azure/identity": "^4.13.1", "@azure/identity": "^4.13.1",
@@ -73,6 +74,22 @@
"node": ">=12.0.0" "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": { "node_modules/@azure/arm-dns": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@azure/arm-dns/-/arm-dns-5.1.0.tgz", "resolved": "https://registry.npmjs.org/@azure/arm-dns/-/arm-dns-5.1.0.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "azure-acme-provisioner", "name": "azure-acme-provisioner",
"version": "0.3.4", "version": "0.4.0",
"author": { "author": {
"name": "Sławomir Koszewski", "name": "Sławomir Koszewski",
"url": "https://github.com/skoszewski" "url": "https://github.com/skoszewski"
@@ -37,6 +37,7 @@
"start:function": "func start" "start:function": "func start"
}, },
"dependencies": { "dependencies": {
"@azure/arm-authorization": "^9.0.0",
"@azure/arm-dns": "^5.1.0", "@azure/arm-dns": "^5.1.0",
"@azure/functions": "^4.14.0", "@azure/functions": "^4.14.0",
"@azure/identity": "^4.13.1", "@azure/identity": "^4.13.1",
+42
View File
@@ -1,9 +1,17 @@
#!/usr/bin/env node #!/usr/bin/env node
import { randomUUID } from 'node:crypto';
import { writeFileSync } from 'node:fs'; import { writeFileSync } from 'node:fs';
import { AuthorizationManagementClient } from '@azure/arm-authorization';
import { DefaultAzureCredential } from '@azure/identity';
import { Command } from 'commander'; import { Command } from 'commander';
import { loadConfig } from './lib/config.js'; import { loadConfig } from './lib/config.js';
import { domainToCertName, Provisioner } from './lib/provisioner.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(); const program = new Command();
program program
@@ -31,6 +39,7 @@ function applyOverrides(options: Record<string, unknown>): void {
const sharedOptions = (cmd: Command): Command => const sharedOptions = (cmd: Command): Command =>
cmd cmd
.option('--keyvault-url <url>', 'Azure KeyVault URL') .option('--keyvault-url <url>', 'Azure KeyVault URL')
.option('--keyvault-resource-group <rg>', 'Resource group containing the Key Vault')
.option('--subscription-id <id>', 'Azure subscription ID') .option('--subscription-id <id>', 'Azure subscription ID')
.option('--resource-group <rg>', 'Resource group to scan (repeatable)', collect, []) .option('--resource-group <rg>', 'Resource group to scan (repeatable)', collect, [])
.option('--dns-zone <zone>', 'Restrict to specific DNS zone (repeatable)', collect, []) .option('--dns-zone <zone>', 'Restrict to specific DNS zone (repeatable)', collect, [])
@@ -120,6 +129,39 @@ sharedOptions(
if (result.errors.length > 0) process.exit(1); if (result.errors.length > 0) process.exit(1);
}); });
sharedOptions(
program
.command('assign-role <domain>')
.description('Assign Key Vault Certificate User and Secrets User roles to a principal for a domain certificate')
.requiredOption('--principal-id <id>', 'Azure principal ID to assign roles to')
).action(async (domain: string, options: Record<string, unknown>) => {
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( sharedOptions(
program program
.command('download <domain>') .command('download <domain>')