diff --git a/.gitignore b/.gitignore index 3e759b7..67cc688 100644 --- a/.gitignore +++ b/.gitignore @@ -328,3 +328,4 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ +/.claude diff --git a/AcmeCaPlugin/AcmeCaPlugin.cs b/AcmeCaPlugin/AcmeCaPlugin.cs index fa1a5cf..c64b8f9 100644 --- a/AcmeCaPlugin/AcmeCaPlugin.cs +++ b/AcmeCaPlugin/AcmeCaPlugin.cs @@ -262,6 +262,9 @@ public async Task Enroll( // Create order var order = await acmeClient.CreateOrderAsync(identifiers, null); + _logger.LogInformation("Order created. OrderUrl: {OrderUrl}, Status: {Status}", + order.OrderUrl, order.Payload?.Status); + // Store pending order immediately var accountId = accountDetails.Kid.Split('/').Last(); @@ -271,26 +274,33 @@ public async Task Enroll( // Finalize with original CSR bytes order = await acmeClient.FinalizeOrderAsync(order, csrBytes); + // Extract order identifier (path only) for database storage + var orderIdentifier = ExtractOrderIdentifier(order.OrderUrl); + // If order is valid immediately, download cert if (order.Payload?.Status == "valid" && !string.IsNullOrEmpty(order.Payload.Certificate)) { var certBytes = await acmeClient.GetCertificateAsync(order); var certPem = EncodeToPem(certBytes, "CERTIFICATE"); + _logger.LogInformation("✅ Enrollment completed successfully. OrderUrl: {OrderUrl}, CARequestID: {OrderId}, Status: GENERATED", + order.OrderUrl, orderIdentifier); + return new EnrollmentResult { - CARequestID = order.Payload.Finalize, + CARequestID = orderIdentifier, Certificate = certPem, Status = (int)EndEntityStatus.GENERATED }; } else { - _logger.LogInformation("⏳ Order not valid yet — will be synced later. Status: {Status}", order.Payload?.Status); + _logger.LogInformation("⏳ Order not valid yet — will be synced later. OrderUrl: {OrderUrl}, CARequestID: {OrderId}, Status: {Status}", + order.OrderUrl, orderIdentifier, order.Payload?.Status); // Order stays saved for next sync return new EnrollmentResult { - CARequestID = order.Payload.Finalize, + CARequestID = orderIdentifier, Status = (int)EndEntityStatus.FAILED, StatusMessage = "Could not retrieve order in allowed time." }; @@ -314,6 +324,34 @@ public async Task Enroll( + /// + /// Extracts the order path from the full ACME order URL for use as a unique identifier. + /// This removes the scheme, host, and port, keeping only the path portion. + /// + /// Full order URL (e.g., https://dv.acme-v02.api.pki.goog/order/ABC123) + /// Order path without leading slash (e.g., "order/ABC123") + /// + /// Input: "https://dv.acme-v02.api.pki.goog/order/IlYl06mPl5VcAQpx3pzR6w" + /// Output: "order/IlYl06mPl5VcAQpx3pzR6w" + /// + private static string ExtractOrderIdentifier(string orderUrl) + { + if (string.IsNullOrWhiteSpace(orderUrl)) + return orderUrl; + + try + { + var uri = new Uri(orderUrl); + // Remove leading slash and return the path + return uri.AbsolutePath.TrimStart('/'); + } + catch (Exception) + { + // If URL parsing fails, return the original (shouldn't happen with valid ACME URLs) + return orderUrl; + } + } + /// /// Extracts the domain name from X.509 subject string /// diff --git a/AcmeCaPlugin/AcmeCaPlugin.csproj b/AcmeCaPlugin/AcmeCaPlugin.csproj index 1adf8a5..e2a323d 100644 --- a/AcmeCaPlugin/AcmeCaPlugin.csproj +++ b/AcmeCaPlugin/AcmeCaPlugin.csproj @@ -1,6 +1,6 @@ - net6.0;net8.0 + net6.0;net8.0;net10.0 disable disable true @@ -9,23 +9,26 @@ AcmeCaPlugin - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/AcmeCaPlugin/AcmeCaPluginConfig.cs b/AcmeCaPlugin/AcmeCaPluginConfig.cs index ec7dffb..7d803d6 100644 --- a/AcmeCaPlugin/AcmeCaPluginConfig.cs +++ b/AcmeCaPlugin/AcmeCaPluginConfig.cs @@ -60,6 +60,13 @@ public static Dictionary GetPluginAnnotations() DefaultValue = "", Type = "String" }, + ["Google_ServiceAccountKeyJson"] = new PropertyConfigInfo() + { + Comments = "Google Cloud DNS: Service account JSON key content (alternative to file path for containerized deployments)", + Hidden = true, + DefaultValue = "", + Type = "Secret" + }, ["Google_ProjectId"] = new PropertyConfigInfo() { Comments = "Google Cloud DNS: Project ID only if using Google DNS (Optional)", @@ -68,6 +75,15 @@ public static Dictionary GetPluginAnnotations() Type = "String" }, + // Container Deployment + ["AccountStoragePath"] = new PropertyConfigInfo() + { + Comments = "Path for ACME account storage. Defaults to %APPDATA%\\AcmeAccounts on Windows or ./AcmeAccounts in containers.", + Hidden = false, + DefaultValue = "", + Type = "String" + }, + // Cloudflare DNS ["Cloudflare_ApiToken"] = new PropertyConfigInfo() { diff --git a/AcmeCaPlugin/AcmeClientConfig.cs b/AcmeCaPlugin/AcmeClientConfig.cs index b639572..dae2ca3 100644 --- a/AcmeCaPlugin/AcmeClientConfig.cs +++ b/AcmeCaPlugin/AcmeClientConfig.cs @@ -15,6 +15,7 @@ public class AcmeClientConfig // Google Cloud DNS public string Google_ServiceAccountKeyPath { get; set; } = null; + public string Google_ServiceAccountKeyJson { get; set; } = null; public string Google_ProjectId { get; set; } = null; // Cloudflare DNS @@ -34,6 +35,8 @@ public class AcmeClientConfig //IBM NS1 DNS Ns1_ApiKey public string Ns1_ApiKey { get; set; } = null; + // Container Deployment Support + public string AccountStoragePath { get; set; } = null; // RFC 2136 Dynamic DNS (BIND) public string Rfc2136_Server { get; set; } = null; public int Rfc2136_Port { get; set; } = 53; diff --git a/AcmeCaPlugin/Clients/Acme/AccountManager.cs b/AcmeCaPlugin/Clients/Acme/AccountManager.cs index 5345368..07ed3fb 100644 --- a/AcmeCaPlugin/Clients/Acme/AccountManager.cs +++ b/AcmeCaPlugin/Clients/Acme/AccountManager.cs @@ -39,13 +39,32 @@ class AccountManager #region Constructor - public AccountManager(ILogger log, string passphrase = null) + public AccountManager(ILogger log, string passphrase = null, string storagePath = null) { _log = log; _passphrase = passphrase; - _basePath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "AcmeAccounts"); + + if (!string.IsNullOrWhiteSpace(storagePath)) + { + // Use the explicitly configured path + _basePath = storagePath; + } + else + { + // Default: Use platform-appropriate path + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (string.IsNullOrEmpty(appDataPath)) + { + // In containers, APPDATA may not be set; use current directory + _basePath = Path.Combine(Directory.GetCurrentDirectory(), "AcmeAccounts"); + } + else + { + _basePath = Path.Combine(appDataPath, "AcmeAccounts"); + } + } + + _log.LogDebug("Account storage path configured: {BasePath}", _basePath); } #endregion diff --git a/AcmeCaPlugin/Clients/Acme/AcmeClientManager.cs b/AcmeCaPlugin/Clients/Acme/AcmeClientManager.cs index 5cbb1b8..e12e6fa 100644 --- a/AcmeCaPlugin/Clients/Acme/AcmeClientManager.cs +++ b/AcmeCaPlugin/Clients/Acme/AcmeClientManager.cs @@ -65,7 +65,7 @@ public AcmeClientManager(ILogger log, AcmeClientConfig config, HttpClient httpCl _email = config.Email; _eabKid = config.EabKid; _eabHmac = config.EabHmacKey; - _accountManager = new AccountManager(log,config.SignerEncryptionPhrase); + _accountManager = new AccountManager(log, config.SignerEncryptionPhrase, config.AccountStoragePath); _log.LogDebug("AcmeClientManager initialized for directory: {DirectoryUrl}", _directoryUrl); } diff --git a/AcmeCaPlugin/Clients/DNS/DnsProviderFactory.cs b/AcmeCaPlugin/Clients/DNS/DnsProviderFactory.cs index b93c616..b3419e4 100644 --- a/AcmeCaPlugin/Clients/DNS/DnsProviderFactory.cs +++ b/AcmeCaPlugin/Clients/DNS/DnsProviderFactory.cs @@ -15,6 +15,7 @@ public static IDnsProvider Create(AcmeClientConfig config, ILogger logger) case "google": return new GoogleDnsProvider( config.Google_ServiceAccountKeyPath, + config.Google_ServiceAccountKeyJson, config.Google_ProjectId ); diff --git a/AcmeCaPlugin/Clients/DNS/GoogleDnsProvider.cs b/AcmeCaPlugin/Clients/DNS/GoogleDnsProvider.cs index c82de75..951630f 100644 --- a/AcmeCaPlugin/Clients/DNS/GoogleDnsProvider.cs +++ b/AcmeCaPlugin/Clients/DNS/GoogleDnsProvider.cs @@ -9,7 +9,7 @@ /// /// Google Cloud DNS provider implementation for managing DNS TXT records. -/// Supports explicit Service Account key or Workload Identity (Application Default Credentials). +/// Supports explicit Service Account key (file or JSON), or Workload Identity (Application Default Credentials). /// public class GoogleDnsProvider : IDnsProvider { @@ -18,19 +18,26 @@ public class GoogleDnsProvider : IDnsProvider /// /// Initializes a new instance of the GoogleDnsProvider class. - /// If serviceAccountKeyPath is null or empty, uses Application Default Credentials. + /// Credential resolution order: JSON key > File path > Application Default Credentials. /// /// Path to the Service Account JSON key file (optional) + /// Service Account JSON key as a string (optional, for containerized deployments) /// Google Cloud project ID containing the DNS zones - public GoogleDnsProvider(string? serviceAccountKeyPath, string projectId) + public GoogleDnsProvider(string? serviceAccountKeyPath, string? serviceAccountKeyJson, string projectId) { _projectId = projectId; GoogleCredential credential; - if (!string.IsNullOrWhiteSpace(serviceAccountKeyPath)) + if (!string.IsNullOrWhiteSpace(serviceAccountKeyJson)) { - Console.WriteLine("✅ Using explicit Service Account JSON key."); + // JSON key provided directly (for container deployments) + Console.WriteLine("✅ Using Service Account JSON key from configuration."); + credential = GoogleCredential.FromJson(serviceAccountKeyJson); + } + else if (!string.IsNullOrWhiteSpace(serviceAccountKeyPath)) + { + Console.WriteLine("✅ Using Service Account JSON key from file."); credential = GoogleCredential.FromFile(serviceAccountKeyPath); } else diff --git a/README.md b/README.md index 4ab4d6d..2ae7cb4 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ This plugin automates DNS-01 challenges using pluggable DNS provider implementat | Provider | Auth Methods Supported | Config Keys Required | |--------------|-----------------------------------------------|--------------------------------------------------------| -| Google DNS | Service Account Key or ADC | `Google_ServiceAccountKeyPath`, `Google_ProjectId` | +| Google DNS | Service Account Key (file or JSON), or ADC | `Google_ServiceAccountKeyPath`, `Google_ServiceAccountKeyJson`, `Google_ProjectId` | | AWS Route 53 | Access Key/Secret or IAM Role | `AwsRoute53_AccessKey`, `AwsRoute53_SecretKey` | | Azure DNS | Client Secret or Managed Identity | `Azure_TenantId`, `Azure_ClientId`, `Azure_ClientSecret`, `Azure_SubscriptionId` | | Cloudflare | API Token | `Cloudflare_ApiToken` | @@ -130,8 +130,9 @@ This logic is handled by the `DnsVerificationHelper` class and ensures a high-co Each provider supports multiple credential strategies: -- **Google DNS**: - - ✅ **Service Account Key** (via `Google_ServiceAccountKeyPath`) +- **Google DNS**: + - ✅ **Service Account Key File** (via `Google_ServiceAccountKeyPath`) + - ✅ **Service Account Key JSON** (via `Google_ServiceAccountKeyJson` - paste JSON directly) - ✅ **Application Default Credentials** (e.g., GCP Workload Identity or developer auth) - **AWS Route 53**: @@ -340,12 +341,17 @@ This ACME Gateway implementation uses a local file-based store to persist ACME a
📁 Account Directory Structure -Each account is saved in its own directory within: +Each account is saved in its own directory within the configured storage path: ``` -%APPDATA%\AcmeAccounts\{host}_{accountId} +{AccountStoragePath}\{host}_{accountId} ``` +**Default paths:** +- **Windows:** `%APPDATA%\AcmeAccounts\{host}_{accountId}` +- **Containers (when APPDATA unavailable):** `./AcmeAccounts\{host}_{accountId}` +- **Custom:** Set `AccountStoragePath` in the Gateway configuration + Where: - `{host}` is the ACME directory host with dots replaced by dashes (e.g., `acme-zerossl-com`) - `{accountId}` is the final segment of the account's KID URL @@ -456,10 +462,10 @@ This section outlines all required ports, file access, permissions, and validati | Path | Purpose | |----------------------------------------------------|----------------------------------------------| -| `%APPDATA%\AcmeAccounts\` | Default base path for ACME account storage | -| `AcmeAccounts\{account_id}\Registration_v2` | Contains serialized ACME account metadata | -| `AcmeAccounts\{account_id}\Signer_v2` | Contains the encrypted private signer key | -| `AcmeAccounts\default_{host}.txt` | Stores the default account pointer for a given directory | +| `%APPDATA%\AcmeAccounts\` or `AccountStoragePath` | Base path for ACME account storage (configurable) | +| `{base}\{account_id}\Registration_v2` | Contains serialized ACME account metadata | +| `{base}\{account_id}\Signer_v2` | Contains the encrypted private signer key | +| `{base}\default_{host}.txt` | Stores the default account pointer for a given directory | #### File Access & Permissions @@ -469,7 +475,8 @@ This section outlines all required ports, file access, permissions, and validati | Account files | Read/Write| `Read`, `Write` | - Files may be optionally encrypted using AES if a passphrase is configured. -- Ensure the service account under which the orchestrator runs has read/write access to `%APPDATA%` or the custom configured base path. +- Ensure the service account under which the orchestrator runs has read/write access to the configured base path. +- For containers, mount a persistent volume to the `AccountStoragePath` to preserve accounts across restarts.
@@ -496,6 +503,61 @@ This section outlines all required ports, file access, permissions, and validati +--- + +### Container Deployment + +This section covers configuration options specific to containerized deployments (Docker, Kubernetes, etc.). + +
+📁 Configurable Account Storage Path + +By default, the plugin stores ACME accounts in `%APPDATA%\AcmeAccounts` on Windows. In containerized environments, use the `AccountStoragePath` configuration option: + +| Environment | Recommended Path | +|-------------|------------------| +| Docker/Kubernetes | `/data/AcmeAccounts` (mounted volume) | +| Windows Container | `C:\AcmeData\AcmeAccounts` | + +If `AccountStoragePath` is not set and `%APPDATA%` is unavailable, the plugin defaults to `./AcmeAccounts` relative to the working directory. + +
+ +
+🌐 Google Cloud DNS in Containers + +For Google Cloud DNS in container environments, you have three authentication options: + +1. **Workload Identity (GKE)**: No explicit credentials needed; uses pod identity. +2. **JSON key in config**: Paste the service account JSON directly into `Google_ServiceAccountKeyJson`. +3. **Mounted JSON file**: Mount the service account key file and set `Google_ServiceAccountKeyPath`. + +
+ +
+☸️ Kubernetes Deployment Considerations + +When deploying in Kubernetes: + +1. **Persistent Storage**: Use a PersistentVolumeClaim for `AccountStoragePath` to preserve ACME accounts across pod restarts. +2. **Cloud Provider Identity**: Leverage Workload Identity (GKE), IAM Roles for Service Accounts (EKS), or Pod Identity (AKS) for DNS provider authentication. + +**Example PersistentVolumeClaim:** +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: acme-accounts +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi +``` + +
+ ## Installation 1. Install the AnyCA Gateway REST per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm). @@ -619,7 +681,9 @@ This section outlines all required ports, file access, permissions, and validati * **SignerEncryptionPhrase** - Used to encrypt singer information when account is saved to disk (optional) * **DnsProvider** - DNS Provider to use for ACME DNS-01 challenges (options: Google, Cloudflare, AwsRoute53, Azure, Ns1, Rfc2136, Infoblox) * **Google_ServiceAccountKeyPath** - Google Cloud DNS: Path to service account JSON key file only if using Google DNS (Optional) + * **Google_ServiceAccountKeyJson** - Google Cloud DNS: Service account JSON key content (alternative to file path for containerized deployments) * **Google_ProjectId** - Google Cloud DNS: Project ID only if using Google DNS (Optional) + * **AccountStoragePath** - Path for ACME account storage. Defaults to %APPDATA%\AcmeAccounts on Windows or ./AcmeAccounts in containers. * **Cloudflare_ApiToken** - Cloudflare DNS: API Token only if using Cloudflare DNS (Optional) * **Azure_ClientId** - Azure DNS: ClientId only if using Azure DNS and Not Managed Itentity in Azure (Optional) * **Azure_ClientSecret** - Azure DNS: ClientSecret only if using Azure DNS and Not Managed Itentity in Azure (Optional) diff --git a/TestProgram/TestProgram.csproj b/TestProgram/TestProgram.csproj index 127834b..0f93419 100644 --- a/TestProgram/TestProgram.csproj +++ b/TestProgram/TestProgram.csproj @@ -2,16 +2,20 @@ Exe - net6.0;net8.0 + net6.0;net8.0;net10.0 enable enable - + + + + + diff --git a/docsource/configuration.md b/docsource/configuration.md index 73195d3..d4b9357 100644 --- a/docsource/configuration.md +++ b/docsource/configuration.md @@ -67,7 +67,7 @@ This plugin automates DNS-01 challenges using pluggable DNS provider implementat | Provider | Auth Methods Supported | Config Keys Required | |--------------|-----------------------------------------------|--------------------------------------------------------| -| Google DNS | Service Account Key or ADC | `Google_ServiceAccountKeyPath`, `Google_ProjectId` | +| Google DNS | Service Account Key (file or JSON), or ADC | `Google_ServiceAccountKeyPath`, `Google_ServiceAccountKeyJson`, `Google_ProjectId` | | AWS Route 53 | Access Key/Secret or IAM Role | `AwsRoute53_AccessKey`, `AwsRoute53_SecretKey` | | Azure DNS | Client Secret or Managed Identity | `Azure_TenantId`, `Azure_ClientId`, `Azure_ClientSecret`, `Azure_SubscriptionId` | | Cloudflare | API Token | `Cloudflare_ApiToken` | @@ -91,8 +91,9 @@ This logic is handled by the `DnsVerificationHelper` class and ensures a high-co Each provider supports multiple credential strategies: -- **Google DNS**: - - ✅ **Service Account Key** (via `Google_ServiceAccountKeyPath`) +- **Google DNS**: + - ✅ **Service Account Key File** (via `Google_ServiceAccountKeyPath`) + - ✅ **Service Account Key JSON** (via `Google_ServiceAccountKeyJson` - paste JSON directly) - ✅ **Application Default Credentials** (e.g., GCP Workload Identity or developer auth) - **AWS Route 53**: @@ -302,12 +303,17 @@ This ACME Gateway implementation uses a local file-based store to persist ACME a
📁 Account Directory Structure -Each account is saved in its own directory within: +Each account is saved in its own directory within the configured storage path: ``` -%APPDATA%\AcmeAccounts\{host}_{accountId} +{AccountStoragePath}\{host}_{accountId} ``` +**Default paths:** +- **Windows:** `%APPDATA%\AcmeAccounts\{host}_{accountId}` +- **Containers (when APPDATA unavailable):** `./AcmeAccounts\{host}_{accountId}` +- **Custom:** Set `AccountStoragePath` in the Gateway configuration + Where: - `{host}` is the ACME directory host with dots replaced by dashes (e.g., `acme-zerossl-com`) - `{accountId}` is the final segment of the account's KID URL @@ -418,10 +424,10 @@ This section outlines all required ports, file access, permissions, and validati | Path | Purpose | |----------------------------------------------------|----------------------------------------------| -| `%APPDATA%\AcmeAccounts\` | Default base path for ACME account storage | -| `AcmeAccounts\{account_id}\Registration_v2` | Contains serialized ACME account metadata | -| `AcmeAccounts\{account_id}\Signer_v2` | Contains the encrypted private signer key | -| `AcmeAccounts\default_{host}.txt` | Stores the default account pointer for a given directory | +| `%APPDATA%\AcmeAccounts\` or `AccountStoragePath` | Base path for ACME account storage (configurable) | +| `{base}\{account_id}\Registration_v2` | Contains serialized ACME account metadata | +| `{base}\{account_id}\Signer_v2` | Contains the encrypted private signer key | +| `{base}\default_{host}.txt` | Stores the default account pointer for a given directory | #### File Access & Permissions @@ -431,7 +437,8 @@ This section outlines all required ports, file access, permissions, and validati | Account files | Read/Write| `Read`, `Write` | - Files may be optionally encrypted using AES if a passphrase is configured. -- Ensure the service account under which the orchestrator runs has read/write access to `%APPDATA%` or the custom configured base path. +- Ensure the service account under which the orchestrator runs has read/write access to the configured base path. +- For containers, mount a persistent volume to the `AccountStoragePath` to preserve accounts across restarts.
@@ -458,6 +465,61 @@ This section outlines all required ports, file access, permissions, and validati +--- + +### Container Deployment + +This section covers configuration options specific to containerized deployments (Docker, Kubernetes, etc.). + +
+📁 Configurable Account Storage Path + +By default, the plugin stores ACME accounts in `%APPDATA%\AcmeAccounts` on Windows. In containerized environments, use the `AccountStoragePath` configuration option: + +| Environment | Recommended Path | +|-------------|------------------| +| Docker/Kubernetes | `/data/AcmeAccounts` (mounted volume) | +| Windows Container | `C:\AcmeData\AcmeAccounts` | + +If `AccountStoragePath` is not set and `%APPDATA%` is unavailable, the plugin defaults to `./AcmeAccounts` relative to the working directory. + +
+ +
+🌐 Google Cloud DNS in Containers + +For Google Cloud DNS in container environments, you have three authentication options: + +1. **Workload Identity (GKE)**: No explicit credentials needed; uses pod identity. +2. **JSON key in config**: Paste the service account JSON directly into `Google_ServiceAccountKeyJson`. +3. **Mounted JSON file**: Mount the service account key file and set `Google_ServiceAccountKeyPath`. + +
+ +
+☸️ Kubernetes Deployment Considerations + +When deploying in Kubernetes: + +1. **Persistent Storage**: Use a PersistentVolumeClaim for `AccountStoragePath` to preserve ACME accounts across pod restarts. +2. **Cloud Provider Identity**: Leverage Workload Identity (GKE), IAM Roles for Service Accounts (EKS), or Pod Identity (AKS) for DNS provider authentication. + +**Example PersistentVolumeClaim:** +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: acme-accounts +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi +``` + +
+ ## Gateway Registration diff --git a/integration-manifest.json b/integration-manifest.json index 28b68da..2b36b4e 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -41,10 +41,18 @@ "name": "Google_ServiceAccountKeyPath", "description": "Google Cloud DNS: Path to service account JSON key file only if using Google DNS (Optional)" }, + { + "name": "Google_ServiceAccountKeyJson", + "description": "Google Cloud DNS: Service account JSON key content (alternative to file path for containerized deployments)" + }, { "name": "Google_ProjectId", "description": "Google Cloud DNS: Project ID only if using Google DNS (Optional)" }, + { + "name": "AccountStoragePath", + "description": "Path for ACME account storage. Defaults to %APPDATA%\\AcmeAccounts on Windows or ./AcmeAccounts in containers." + }, { "name": "Cloudflare_ApiToken", "description": "Cloudflare DNS: API Token only if using Cloudflare DNS (Optional)"