feat: initialize azure-acme-provisioner project with core functionality
- Add package.json for project metadata and dependencies - Implement CLI in src/cli.ts for managing SSL/TLS certificates - Create Azure Functions host configuration in src/function/host.json - Set up timer function in src/function/index.ts for scheduled certificate management - Define configuration loading and error handling in src/lib/config.ts - Implement DNS zone scanning and challenge management in src/lib/dns.ts - Develop ACME client for certificate issuance in src/lib/acme.ts - Create KeyVault store for managing secrets and certificates in src/lib/keyvault.ts - Implement provisioning logic in src/lib/provisioner.ts for issuing and renewing certificates - Add TypeScript configuration files for building the project
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
import { DnsManagementClient } from '@azure/arm-dns';
|
||||
import { TokenCredential } from '@azure/identity';
|
||||
import { Config } from './config.js';
|
||||
|
||||
export interface DomainRecord {
|
||||
fqdn: string;
|
||||
zoneName: string;
|
||||
resourceGroup: string;
|
||||
isWildcard: boolean;
|
||||
}
|
||||
|
||||
export async function scanDnsZones(
|
||||
credential: TokenCredential,
|
||||
config: Config
|
||||
): Promise<DomainRecord[]> {
|
||||
const client = new DnsManagementClient(credential, config.subscriptionId);
|
||||
const results: DomainRecord[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const rg of config.resourceGroups) {
|
||||
for await (const zone of client.zones.listByResourceGroup(rg)) {
|
||||
if (!zone.name) continue;
|
||||
if (config.dnsZones && !config.dnsZones.includes(zone.name)) continue;
|
||||
|
||||
if (isAcmeTagged(zone.tags)) {
|
||||
addDomain(results, seen, zone.name, rg, false);
|
||||
addDomain(results, seen, `*.${zone.name}`, rg, true);
|
||||
}
|
||||
|
||||
for await (const record of client.recordSets.listAllByDnsZone(rg, zone.name)) {
|
||||
if (!record.name) continue;
|
||||
if (!isAcmeTagged(record.metadata)) continue;
|
||||
if (record.type !== 'Microsoft.Network/dnszones/A' &&
|
||||
record.type !== 'Microsoft.Network/dnszones/CNAME') continue;
|
||||
|
||||
const fqdn = record.name === '@' ? zone.name : `${record.name}.${zone.name}`;
|
||||
addDomain(results, seen, fqdn, rg, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function addDomain(
|
||||
results: DomainRecord[],
|
||||
seen: Set<string>,
|
||||
fqdn: string,
|
||||
resourceGroup: string,
|
||||
isWildcard: boolean
|
||||
): void {
|
||||
if (seen.has(fqdn)) return;
|
||||
seen.add(fqdn);
|
||||
const zoneName = fqdn.replace(/^\*\./, '');
|
||||
results.push({ fqdn, zoneName, resourceGroup, isWildcard });
|
||||
}
|
||||
|
||||
function isAcmeTagged(tags: Record<string, string> | undefined): boolean {
|
||||
if (!tags) return false;
|
||||
const val = tags['acme'];
|
||||
return val === 'true' || val === 'enabled';
|
||||
}
|
||||
|
||||
export class DnsChallengeManager {
|
||||
private readonly client: DnsManagementClient;
|
||||
|
||||
constructor(credential: TokenCredential, private readonly config: Config) {
|
||||
this.client = new DnsManagementClient(credential, config.subscriptionId);
|
||||
}
|
||||
|
||||
async createTxtRecord(fqdn: string, value: string): Promise<void> {
|
||||
const { resourceGroup, zone, name } = this.parseFqdn(fqdn);
|
||||
await this.client.recordSets.createOrUpdate(resourceGroup, zone, name, 'TXT', {
|
||||
ttl: this.config.dnsChallengeTtl,
|
||||
txtRecords: [{ value: [value] }],
|
||||
});
|
||||
}
|
||||
|
||||
async deleteTxtRecord(fqdn: string): Promise<void> {
|
||||
const { resourceGroup, zone, name } = this.parseFqdn(fqdn);
|
||||
try {
|
||||
await this.client.recordSets.delete(resourceGroup, zone, name, 'TXT');
|
||||
} catch {
|
||||
// best-effort cleanup; ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
private parseFqdn(fqdn: string): { resourceGroup: string; zone: string; name: string } {
|
||||
for (const rg of this.config.resourceGroups) {
|
||||
for (const zone of this.config.dnsZones ?? []) {
|
||||
if (fqdn.endsWith(`.${zone}`) || fqdn === zone) {
|
||||
const name = fqdn === zone ? '@' : fqdn.slice(0, -(zone.length + 1));
|
||||
return { resourceGroup: rg, zone, name };
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback: derive zone from fqdn by stripping first label
|
||||
const parts = fqdn.split('.');
|
||||
const zone = parts.slice(1).join('.');
|
||||
const name = parts[0];
|
||||
const rg = this.config.resourceGroups[0];
|
||||
return { resourceGroup: rg, zone, name };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user