diff --git a/README.md b/README.md index f4d9ac6..a2fbc99 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ Commands: run Scan DNS zones and issue or renew certificates (default) 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 + renew Force-renew a certificate for a specific domain + download Download the PEM bundle for a domain from Key Vault Common options: --keyvault-url Azure KeyVault URL @@ -77,6 +78,18 @@ azure-acme-provisioner run --http 8080 > **Note:** Binding port 80 requires root privileges or `CAP_NET_BIND_SERVICE`. When running in Docker, map the host port to the container: `-p 80:8080` and pass `--http 8080`. +### Downloading certificates + +The `download` command fetches the PEM bundle (private key + certificate + chain) from Key Vault and writes it to stdout or a file: + +```sh +# Print to stdout +azure-acme-provisioner download api.example.com + +# Write to a file +azure-acme-provisioner download api.example.com --output api.example.com.pem +``` + ## Configuration All configuration is via environment variables. CLI flags override env vars when both are provided. diff --git a/src/cli.ts b/src/cli.ts index 91b768c..55b3fa7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { writeFileSync } from 'node:fs'; import { Command } from 'commander'; import { loadConfig } from './lib/config.js'; import { domainToCertName, Provisioner } from './lib/provisioner.js'; @@ -119,6 +120,24 @@ sharedOptions( if (result.errors.length > 0) process.exit(1); }); +sharedOptions( + program + .command('download ') + .description('Download the PEM bundle (private key + certificate + chain) for a domain') + .option('--output ', 'Write to file instead of stdout') +).action(async (domain: string, options: Record) => { + applyOverrides(options); + const config = loadConfig(); + const provisioner = new Provisioner(config); + const pem = await provisioner.download(domain); + if (options['output']) { + writeFileSync(String(options['output']), pem, 'utf8'); + console.log(`Certificate written to ${options['output']}`); + } else { + process.stdout.write(pem); + } +}); + program.parseAsync(process.argv).catch((err: unknown) => { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); diff --git a/src/lib/provisioner.ts b/src/lib/provisioner.ts index 6a52183..45c2d87 100644 --- a/src/lib/provisioner.ts +++ b/src/lib/provisioner.ts @@ -131,6 +131,13 @@ export class Provisioner { return result; } + async download(domain: string): Promise { + const certName = domainToCertName(domain); + const pem = await this.store.getSecret(certName); + if (!pem) throw new Error(`Certificate not found in KeyVault: ${certName}`); + return pem; + } + async scan(): Promise { return scanDnsZones(this.credential, this.config); }