From e2565e0f4bdd3612c1007d1fbaf35f3d054b50a7 Mon Sep 17 00:00:00 2001 From: Mark Kachkaev Date: Tue, 4 Aug 2026 15:27:29 -0400 Subject: [PATCH 1/6] Cross account discovery added. --- .../workflows/keyfactor-starter-workflow.yml | 13 +- .gitignore | 4 +- CHANGELOG.md | 8 +- README.md | 47 +- aws-acm-orchestrator.sln | 12 +- aws-acm-orchestrator/Jobs/AuthDescription.cs | 53 ++ aws-acm-orchestrator/Jobs/Discovery.cs | 521 ++++++++++++++++++ aws-acm-orchestrator/Jobs/Inventory.cs | 73 ++- aws-acm-orchestrator/Jobs/Management.cs | 129 +++-- aws-acm-orchestrator/Jobs/StorePathParser.cs | 66 +++ aws-acm-orchestrator/manifest.json | 4 + docsource/content.md | 8 + integration-manifest.json | 14 +- .../bash/curl_create_store_types.sh | 13 +- .../restmethod_create_store_types.ps1 | 13 +- 15 files changed, 855 insertions(+), 123 deletions(-) create mode 100644 aws-acm-orchestrator/Jobs/AuthDescription.cs create mode 100644 aws-acm-orchestrator/Jobs/Discovery.cs create mode 100644 aws-acm-orchestrator/Jobs/StorePathParser.cs diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 61ea7a0..b8c72b2 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -1,4 +1,4 @@ -name: Keyfactor Bootstrap Workflow +name: Keyfactor Bootstrap Workflow on: workflow_dispatch: @@ -11,10 +11,9 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@3.1.2 + uses: keyfactor/actions/.github/workflows/starter.yml@v5 secrets: - token: ${{ secrets.V2BUILDTOKEN}} - APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} - gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} - gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} - scan_token: ${{ secrets.SAST_TOKEN }} + token: ${{ secrets.V2BUILDTOKEN}} # REQUIRED + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} # Only required for golang builds + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} # Only required for golang builds + scan_token: ${{ secrets.SAST_TOKEN }} # REQUIRED diff --git a/.gitignore b/.gitignore index 07a4825..0ab729a 100644 --- a/.gitignore +++ b/.gitignore @@ -348,4 +348,6 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ -/aws-acm-orchestrator.sln.licenseheader +/.claude/settings.local.json +/Feature-Spec-Cross-Account-Discovery-1.pdf +/Ticket.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 965a14c..cafdf16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ -3.0.3 -* Bug Fix - On Management Add/renewal jobs, the leaf certificate is no longer included in the `CertificateChain` sent to ACM. BouncyCastle's `GetCertificateChain` returns the leaf as the first element, and it was already sent separately as the certificate body, causing the leaf to appear twice within the published certificate's chain. When the certificate has no intermediates, the chain is now omitted entirely rather than sent empty. +3.1.0 +* Added support for cross-account **Discovery** + * Discovery enumerates AWS Organizations accounts via `organizations:ListAccounts`, assumes a per-account IAM role (configurable via the new `DiscoveryRoleName` property; default `KeyfactorACMDiscoveryRole`), and scans each account's enabled regions for ACM certificates + * Discovered certificate stores use a self-contained `Store Path` of the form `|` that carries both the Role ARN to assume and the AWS Region in a single field; legacy region-only `Store Path` values (with the Role ARN in `Client Machine`) continue to work + * Added the `DiscoveryRoleName` store type property +* Discovery, Inventory, and Management jobs now return descriptive success, warning, and failure messages to Keyfactor Command that identify the exact store (and, for Discovery, the accounts and regions scanned) instead of directing the user to the logs 3.0.2 * Bug Fix - On Management jobs, do not send ACM tags if the certificate is being renewed/replaced diff --git a/README.md b/README.md index 8f3a2fe..c4a3e1e 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

@@ -31,6 +31,14 @@ ## Overview +> [!WARNING] +> +> **The `AWS-ACM-v3` store type identifies each certificate store by a single AWS `Region` in the `Store Path` field, with the full Role ARN in `Client Machine`.** +> +> **New with cross-account Discovery:** `Store Path` now also accepts a **self-contained** value of the form **`|`** — for example `arn:aws:iam::123456789012:role/KeyfactorACMDiscoveryRole|us-east-1` — which carries **both** the Role ARN to assume **and** the Region in a single field. Certificate stores created by cross-account Discovery use this combined format, and for those stores the `Client Machine` field is informational only. Legacy region-only `Store Path` values (with the Role ARN in `Client Machine`) continue to work. +> +> **Migrating from `AWS-ACM`, `AwsCerManO`, or `AwsCerManA`:** those older store types are **not** compatible and must be recreated as `AWS-ACM-v3`. + AWS Certificate Manager is a service that lets you easily provision, manage, and deploy public and private Secure Sockets Layer/Transport Layer Security (SSL/TLS) certificates for use with AWS services and your internal connected resources. SSL/TLS certificates are used to secure network communications and establish the identity of websites over the Internet as well as resources on private networks. @@ -195,7 +203,7 @@ Inventory required actions: |--------------|------------------------------------------------------------------------------------------------------------------------| | Add | ✅ Checked | | Remove | ✅ Checked | -| Discovery | 🔲 Unchecked | +| Discovery | ✅ Checked | | Reenrollment | 🔲 Unchecked | | Create | 🔲 Unchecked | @@ -238,7 +246,7 @@ the Keyfactor Command Portal | Capability | AWS-ACM-v3 | Store type name orchestrator will register with. Check the box to allow entry of value | | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add | | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove | - | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | + | Supports Discovery | ✅ Checked | Check the box. Indicates that the Store Type supports Discovery | | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | | Needs Server | 🔲 Unchecked | Determines if a target server name is required when creating store | @@ -281,6 +289,7 @@ the Keyfactor Command Portal | IAMUserAccessKey | IAM User Access Key | The AWS Access Key for an IAM User | Secret | | 🔲 Unchecked | | IAMUserAccessSecret | IAM User Access Secret | The AWS Access Secret for an IAM User. | Secret | | 🔲 Unchecked | | ExternalId | sts:ExternalId | An optional parameter sts:ExternalId to pass with Assume Role calls | String | | 🔲 Unchecked | + | DiscoveryRoleName | Discovery Role Name | The IAM role name that exists in each target account. Used during cross-account Discovery to construct the Role ARN for cross-account access (arn:aws:iam:::role/). During a Discovery job this is supplied via the 'File name patterns to match' dialog field; this store property documents and defaults the value. | String | KeyfactorACMDiscoveryRole | 🔲 Unchecked | The Custom Fields tab should look like this: @@ -383,6 +392,14 @@ the Keyfactor Command Portal + ###### Discovery Role Name + The IAM role name that exists in each target account. Used during cross-account Discovery to construct the Role ARN for cross-account access (arn:aws:iam:::role/). During a Discovery job this is supplied via the 'File name patterns to match' dialog field; this store property documents and defaults the value. + + ![AWS-ACM-v3 Custom Field - DiscoveryRoleName](docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-dialog.png) + ![AWS-ACM-v3 Custom Field - DiscoveryRoleName](docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-validation-options-dialog.png) + + + ##### Entry Parameters Tab @@ -410,18 +427,16 @@ the Keyfactor Command Portal 1. **Download the latest AWS Certificate Manager (ACM) Universal Orchestrator extension from GitHub.** - Navigate to the [AWS Certificate Manager (ACM) Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/aws-orchestrator/releases/latest). Refer to the compatibility matrix below to determine the asset should be downloaded. Then, click the corresponding asset to download the zip archive. + Navigate to the [AWS Certificate Manager (ACM) Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/aws-acm-orchestrator-dev/releases/latest). Refer to the compatibility matrix below to determine the asset should be downloaded. Then, click the corresponding asset to download the zip archive. - | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `aws-orchestrator` .NET version to download | + | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `aws-acm-orchestrator-dev` .NET version to download | | --------- | ----------- | ----------- | ----------- | - | Older than `11.0.0` | | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` || Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | | `11.6` _and_ newer | `net8.0` | | `net8.0` | Unzip the archive containing extension assemblies to a known location. - > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net6.0`. + > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net8.0`. 2. **Locate the Universal Orchestrator extensions directory.** @@ -430,10 +445,10 @@ the Keyfactor Command Portal 3. **Create a new directory for the AWS Certificate Manager (ACM) Universal Orchestrator extension inside the extensions directory.** - Create a new directory called `aws-orchestrator`. + Create a new directory called `aws-acm-orchestrator-dev`. > The directory name does not need to match any names used elsewhere; it just has to be unique within the extensions directory. -4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `aws-orchestrator` directory.** +4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `aws-acm-orchestrator-dev` directory.** 5. **Restart the Universal Orchestrator service.** @@ -482,7 +497,7 @@ When migrating to the `AWS-ACM-v3` type please note that field usage has changed | Category | Select "AWS Certificate Manager v3" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | This is a full AWS ARN specifying a Role. This is the Role that will be assumed in any Auth scenario performing Assume Role. This will dictate what certificates are usable by the orchestrator. A preceding [profile] name should be included if a Credential Profile is to be used in Default Sdk Auth. | - | Store Path | A single specified AWS Region the store will operate in. Additional regions should get their own store defined. | + | Store Path | The AWS Region the store operates in (e.g. us-east-1). Stores created by cross-account Discovery instead use a self-contained path of the form '|' (e.g. 'arn:aws:iam::123456789012:role/KeyfactorACMDiscoveryRole|us-east-1') which carries both the Role ARN to assume and the Region; for those stores the Client Machine field is informational only. Additional regions should get their own store defined. | | Orchestrator | Select an approved orchestrator capable of managing `AWS-ACM-v3` certificates. Specifically, one with the `AWS-ACM-v3` capability. | | UseDefaultSdkAuth | A switch to enable the store to use Default SDK credentials | | DefaultSdkAssumeRole | A switch to enable the store to assume a new Role when using Default SDK credentials | @@ -496,6 +511,7 @@ When migrating to the `AWS-ACM-v3` type please note that field usage has changed | IAMUserAccessKey | The AWS Access Key for an IAM User | | IAMUserAccessSecret | The AWS Access Secret for an IAM User. | | ExternalId | An optional parameter sts:ExternalId to pass with Assume Role calls | + | DiscoveryRoleName | The IAM role name that exists in each target account. Used during cross-account Discovery to construct the Role ARN for cross-account access (arn:aws:iam:::role/). During a Discovery job this is supplied via the 'File name patterns to match' dialog field; this store property documents and defaults the value. | @@ -519,7 +535,7 @@ When migrating to the `AWS-ACM-v3` type please note that field usage has changed | Category | Select "AWS Certificate Manager v3" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | This is a full AWS ARN specifying a Role. This is the Role that will be assumed in any Auth scenario performing Assume Role. This will dictate what certificates are usable by the orchestrator. A preceding [profile] name should be included if a Credential Profile is to be used in Default Sdk Auth. | - | Store Path | A single specified AWS Region the store will operate in. Additional regions should get their own store defined. | + | Store Path | The AWS Region the store operates in (e.g. us-east-1). Stores created by cross-account Discovery instead use a self-contained path of the form '|' (e.g. 'arn:aws:iam::123456789012:role/KeyfactorACMDiscoveryRole|us-east-1') which carries both the Role ARN to assume and the Region; for those stores the Client Machine field is informational only. Additional regions should get their own store defined. | | Orchestrator | Select an approved orchestrator capable of managing `AWS-ACM-v3` certificates. Specifically, one with the `AWS-ACM-v3` capability. | | Properties.UseDefaultSdkAuth | A switch to enable the store to use Default SDK credentials | | Properties.DefaultSdkAssumeRole | A switch to enable the store to assume a new Role when using Default SDK credentials | @@ -533,6 +549,7 @@ When migrating to the `AWS-ACM-v3` type please note that field usage has changed | Properties.IAMUserAccessKey | The AWS Access Key for an IAM User | | Properties.IAMUserAccessSecret | The AWS Access Secret for an IAM User. | | Properties.ExternalId | An optional parameter sts:ExternalId to pass with Assume Role calls | + | Properties.DiscoveryRoleName | The IAM role name that exists in each target account. Used during cross-account Discovery to construct the Role ARN for cross-account access (arn:aws:iam:::role/). During a Discovery job this is supplied via the 'File name patterns to match' dialog field; this store property documents and defaults the value. | 3. **Import the CSV file to create the certificate stores** diff --git a/aws-acm-orchestrator.sln b/aws-acm-orchestrator.sln index bc18a90..9629a29 100644 --- a/aws-acm-orchestrator.sln +++ b/aws-acm-orchestrator.sln @@ -1,14 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30717.126 +# Visual Studio Version 18 +VisualStudioVersion = 18.8.12023.21 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "aws-acm-orchestrator", "aws-acm-orchestrator\aws-acm-orchestrator.csproj", "{9BEDC094-06DC-40C6-B5DF-88B4960D4DCF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "aws-acm-orchestrator.Tests", "aws-acm-orchestrator.Tests\aws-acm-orchestrator.Tests.csproj", "{A1B2C3D4-E5F6-47A8-9B0C-1D2E3F4A5B6C}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{515E06CB-9355-43EA-AF70-27F066A5C3AF}" ProjectSection(SolutionItems) = preProject + docsource\aws-acm-v3.md = docsource\aws-acm-v3.md + docsource\content.md = docsource\content.md integration-manifest.json = integration-manifest.json readme_source.md = readme_source.md sample-manifest.json = sample-manifest.json @@ -47,10 +47,6 @@ Global {9BEDC094-06DC-40C6-B5DF-88B4960D4DCF}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BEDC094-06DC-40C6-B5DF-88B4960D4DCF}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BEDC094-06DC-40C6-B5DF-88B4960D4DCF}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1D2E3F4A5B6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1D2E3F4A5B6C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1D2E3F4A5B6C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1D2E3F4A5B6C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/aws-acm-orchestrator/Jobs/AuthDescription.cs b/aws-acm-orchestrator/Jobs/AuthDescription.cs new file mode 100644 index 0000000..452b222 --- /dev/null +++ b/aws-acm-orchestrator/Jobs/AuthDescription.cs @@ -0,0 +1,53 @@ +// Copyright 2025 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Keyfactor.Extensions.Aws.Models; + +namespace Keyfactor.Extensions.Orchestrator.Aws.Acm.Jobs +{ + ///

+ /// Produces the canonical name of the AWS credential method a store/job is configured for, + /// derived from its auth custom fields. This lets every job log which authentication + /// path it actually took (in the same vocabulary the auth library uses) instead of leaving the + /// operator to infer it, and gives automated tests a stable, greppable marker. + /// + /// A Credential-Profile store is distinguished from a plain Default-SDK store by the "[profile]" + /// prefix the auth library expects on the role/ARN value. + /// + internal static class AuthDescription + { + public static string DescribeMethod(AuthCustomFieldParameters cf, string roleArn) + { + if (cf == null) + return "Unknown"; + + if (cf.UseOAuth) + return "OAuthProvider"; + + if (cf.UseIAM) + return "IamUser"; + + if (cf.UseDefaultSdkAuth) + { + bool usesProfile = !string.IsNullOrWhiteSpace(roleArn) && roleArn.TrimStart().StartsWith("["); + string method = usesProfile ? "DefaultSdk_CredentialProfile" : "DefaultSdk"; + if (cf.DefaultSdkAssumeRole) + method += "_AssumeRole"; + return method; + } + + return "Unknown"; + } + } +} diff --git a/aws-acm-orchestrator/Jobs/Discovery.cs b/aws-acm-orchestrator/Jobs/Discovery.cs new file mode 100644 index 0000000..1549d1f --- /dev/null +++ b/aws-acm-orchestrator/Jobs/Discovery.cs @@ -0,0 +1,521 @@ +// Copyright 2025 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Amazon; +using Amazon.CertificateManager; +using Amazon.CertificateManager.Model; +using Amazon.EC2; +using Amazon.EC2.Model; +using Amazon.Organizations; +using Amazon.Organizations.Model; +using Amazon.Runtime; +using Amazon.Runtime.Internal.Util; +using Amazon.SecurityToken; +using Amazon.SecurityToken.Model; +using Keyfactor.Extensions.Aws; +using Keyfactor.Extensions.Aws.Models; +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions; +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; + +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Keyfactor.Extensions.Orchestrator.Aws.Acm.Jobs +{ + public class Discovery : IDiscoveryJobExtension + { + // The default IAM role name expected to exist in every member account. Operators can override + // it (see DiscoveryRoleName resolution below). Matches the integration manifest default. + internal const string DefaultDiscoveryRoleName = "KeyfactorACMDiscoveryRole"; + + public string ExtensionName => "AWS-ACM-v3"; + internal ILogger Logger; + internal IPAMSecretResolver PamSecretResolver; + internal AwsAuthUtility AuthUtilities; + + public Discovery(IPAMSecretResolver pam, ILogger logger) + { + PamSecretResolver = pam; + Logger = logger; + AuthUtilities = new AwsAuthUtility(pam); + } + + public JobResult ProcessJob(DiscoveryJobConfiguration jobConfiguration, SubmitDiscoveryUpdate submitDiscovery) + { + Logger.MethodEntry(); + + // JobProperties is Dictionary on DiscoveryJobConfiguration; round-trip through JSON + // to produce the AuthCustomFieldParameters that the auth library expects. + Logger.LogTrace("Deserializing Job Properties to AuthCustomFieldParameters object."); + string jobPropsJson = JsonConvert.SerializeObject(jobConfiguration.JobProperties); + AuthCustomFieldParameters customFields = JsonConvert.DeserializeObject( + jobPropsJson, + new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); + + // The standard Keyfactor Discovery dialog exposes no auth-method fields (those live on the + // store type, which Discovery doesn't have yet), so an operator-launched Discovery arrives + // with all auth booleans false and AwsAuthUtility rejects it as "No Auth method selected". + // Default to Default SDK auth using the host's ambient credentials (EC2 instance profile, + // ~/.aws/credentials, AWS_* env). Whether to STS-assume into another role is decided below + // once we know if "Directories to search" supplied a target ARN. + bool defaultedAuth = false; + if (!customFields.UseDefaultSdkAuth && !customFields.UseOAuth && !customFields.UseIAM) + { + Logger.LogInformation("No auth method selected in Discovery JobProperties; defaulting to Default SDK auth."); + customFields.UseDefaultSdkAuth = true; + defaultedAuth = true; + } + Logger.LogTrace("Deserialized Job Properties."); + + // DiscoveryJobConfiguration has no StorePath. Use a "Region" key from JobProperties as the + // base region for credential resolution, or fall back to us-east-1. + string baseRegion = GetJobProperty(jobConfiguration.JobProperties, "Region") ?? "us-east-1"; + + // The management-account Role ARN to assume comes from the Discovery dialog's "Directories to + // search" field. Standard Discovery dialog fields are packed into JobProperties at submission + // time with a key name that varies across Keyfactor Command versions (dirs / Dirs / + // DirectoriesToSearch), so search case-insensitively across the known variants. Falls back to + // ClientMachine for older callers that wired the ARN there directly. + string dirsValue = jobConfiguration.JobProperties? + .Where(kv => kv.Key.Equals("dirs", StringComparison.OrdinalIgnoreCase) + || kv.Key.Equals("DirectoriesToSearch", StringComparison.OrdinalIgnoreCase)) + .Select(kv => kv.Value?.ToString()) + .FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)); + + string roleArn = dirsValue? + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .FirstOrDefault(s => s.Length > 0); + + if (string.IsNullOrWhiteSpace(roleArn)) + { + roleArn = jobConfiguration.ClientMachine; + } + + // When we defaulted to Default SDK auth, only STS-assume if "Directories to search" + // supplied a target ARN that looks like an IAM role (not ClientMachine, which the + // orchestrator framework fills with the agent identifier like "UOLinux"). Otherwise + // use the host's ambient credentials as-is — the case where the EC2 instance role + // already holds the needed Organizations/ACM permissions and there's nothing to assume. + bool dirsHasRoleArn = !string.IsNullOrWhiteSpace(dirsValue) + && (dirsValue.StartsWith("arn:aws:iam:", StringComparison.OrdinalIgnoreCase) + || dirsValue.StartsWith("arn:aws-us-gov:iam:", StringComparison.OrdinalIgnoreCase) + || dirsValue.StartsWith("arn:aws-cn:iam:", StringComparison.OrdinalIgnoreCase)); + if (defaultedAuth) + { + customFields.DefaultSdkAssumeRole = dirsHasRoleArn; + Logger.LogDebug($"DefaultSdkAssumeRole resolved to {customFields.DefaultSdkAssumeRole} based on 'Directories to search'."); + } + + if (jobConfiguration.JobProperties != null) + { + foreach (var kv in jobConfiguration.JobProperties) + { + Logger.LogDebug($"JobProperties[{kv.Key}] = {kv.Value}"); + } + } + Logger.LogInformation($"Discovery base (management) credentials will assume role: {roleArn}"); + + AuthenticationParameters authParams = new AuthenticationParameters + { + RoleARN = roleArn, + Region = baseRegion, + CustomFields = customFields + }; + + Logger.LogTrace("Resolving AWS Credentials object."); + AwsExtensionCredential providedCredentials; + try + { + providedCredentials = AuthUtilities.GetCredentials(authParams); + } + catch (Exception ex) + { + Logger.LogError("An error occurred while trying to get AWS Credentials."); + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = jobConfiguration.JobHistoryId, + FailureMessage = ex.Message + }; + } + string authMethod = AuthDescription.DescribeMethod(customFields, roleArn); + Logger.LogInformation($"AWS credential method resolved: [{authMethod}] (base role/profile: {roleArn}, region: {baseRegion})."); + Logger.LogTrace("AWS Credentials resolved. Performing Discovery."); + + return PerformDiscovery(providedCredentials, jobConfiguration, submitDiscovery); + } + + internal JobResult PerformDiscovery(AwsExtensionCredential awsCredentials, + DiscoveryJobConfiguration config, SubmitDiscoveryUpdate submitDiscovery) + { + Logger.MethodEntry(); + var warnings = new List(); // specific account/region targets that failed to scan + int accountsScanned = 0; + var discoveredStores = new List(); + + // ----- Resolve discovery inputs ----- + // Base region drives the partition (commercial / GovCloud / China) and credential resolution. + string baseRegionName = GetJobProperty(config.JobProperties, "Region") ?? "us-east-1"; + RegionEndpoint baseRegion = RegionEndpoint.GetBySystemName(baseRegionName); + + // The IAM role name to assume in each member account. Operators may override via an explicit + // "DiscoveryRoleName" job property, or via the "File name patterns to match" dialog field + // (packed as "patterns"/"namepatterns"); otherwise the manifest default is used. + string discoveryRoleName = FirstNonEmpty( + GetJobPropertyMulti(config.JobProperties, "DiscoveryRoleName", "patterns", "namepatterns"), + DefaultDiscoveryRoleName); + + // Optional sts:ExternalId for the cross-account AssumeRole calls. No standard dialog field maps + // to this, so it can only be supplied programmatically via an "ExternalId" job property. + string externalId = GetJobProperty(config.JobProperties, "ExternalId"); + + // Optional comma-separated list of account IDs to skip. Operators may supply it via an explicit + // "IgnoredAccounts" job property or the "Directories to ignore" dialog field ("ignoreddirs"). + var ignoredAccounts = new HashSet( + SplitCsv(GetJobPropertyMulti(config.JobProperties, "IgnoredAccounts", "ignoreddirs", "DirectoriesToIgnore")), + StringComparer.OrdinalIgnoreCase); + + // Regions to scan. Explicit "Regions" job property wins; otherwise the "Extensions" dialog field + // ("extensions") can carry a comma-separated list; otherwise every region in the base partition. + // When the operator pins a list we use it verbatim; otherwise (blank or "*") we auto-detect the + // enabled regions per account below so each account is scanned against its own enabled set. + string regionsConfig = GetJobPropertyMulti(config.JobProperties, "Regions", "extensions"); + bool autoDetectRegions = string.IsNullOrWhiteSpace(regionsConfig) || regionsConfig.Trim() == "*"; + var regions = ParseRegions(regionsConfig, baseRegion.PartitionName); + + Logger.LogDebug($"Discovery candidate region set: {regions.Count} region(s) in partition '{baseRegion.PartitionName}'. " + + $"Per-account auto-detection: {autoDetectRegions}. Member-account role name: '{discoveryRoleName}'."); + + // ----- Enumerate accounts via AWS Organizations ----- + // If the base credentials belong to an Organizations management/delegated account with + // organizations:ListAccounts, we enumerate every account and probe each. If that fails (not an + // org, AccessDenied, single-account setup, etc.) we fall back to scanning the base account only, + // so existing single-account discovery keeps working. + List accountIds = TryListOrganizationAccounts(awsCredentials.GetAwsCredentialObject(), baseRegion); + + if (accountIds == null) + { + Logger.LogInformation("AWS Organizations enumeration unavailable; falling back to single-account discovery " + + "using the resolved base credentials."); + accountsScanned = 1; + var baseRegions = ResolveAccountRegions(awsCredentials.GetAwsCredentialObject(), regions, + autoDetectRegions, baseRegion, "Base account"); + ScanRegionsSingleAccount(awsCredentials.GetAwsCredentialObject(), baseRegions, discoveredStores, warnings); + } + else + { + Logger.LogInformation($"AWS Organizations returned {accountIds.Count} active account(s). Beginning cross-account scan."); + foreach (string accountId in accountIds) + { + if (ignoredAccounts.Contains(accountId)) + { + Logger.LogDebug($"Skipping account {accountId} (in ignore list)."); + continue; + } + + string targetRoleArn = BuildMemberRoleArn(baseRegion.PartitionName, accountId, discoveryRoleName); + + AWSCredentials targetCreds; + try + { + targetCreds = AssumeRole(awsCredentials.GetAwsCredentialObject(), baseRegion, targetRoleArn, externalId); + } + catch (Exception ex) + { + // Account may not have the discovery role deployed yet, or trust isn't configured. + Logger.LogWarning($"Could not assume {targetRoleArn} in account {accountId}: {ex.Message}"); + warnings.Add($"account {accountId} (could not assume {targetRoleArn}: {ex.Message})"); + continue; + } + + accountsScanned++; + + // Detect the regions enabled in this specific account (unless the operator pinned a list). + var accountRegions = ResolveAccountRegions(targetCreds, regions, autoDetectRegions, baseRegion, + $"Account {accountId}"); + + foreach (var region in accountRegions) + { + try + { + if (RegionHasCertificates(targetCreds, region)) + { + string storePath = StorePathParser.Build(targetRoleArn, region.SystemName); + Logger.LogInformation($"Discovered ACM store in account {accountId} region {region.SystemName}. " + + $"Discovered store path: {storePath}"); + discoveredStores.Add(storePath); + } + } + catch (Exception ex) + { + // Region disabled in this account, missing ACM permission, etc. Log and continue. + Logger.LogWarning($"Could not check ACM in account {accountId} region {region.SystemName}: {ex.Message}"); + warnings.Add($"account {accountId} region {region.SystemName} ({ex.Message})"); + } + } + } + } + + bool crossAccount = accountIds != null; + string scope = crossAccount + ? $"scanned {accountsScanned} of {accountIds.Count} organization account(s)" + : "scanned the base account (AWS Organizations enumeration was unavailable)"; + string summary = $"Discovery complete: {scope} and found {discoveredStores.Count} ACM certificate store(s) with certificates, " + + "now reported to Keyfactor Command."; + if (warnings.Count > 0) + { + int show = Math.Min(warnings.Count, 5); + string affected = string.Join("; ", warnings.Take(show)); + if (warnings.Count > show) + affected += $"; and {warnings.Count - show} more"; + summary += $" {warnings.Count} target(s) could not be scanned: {affected}."; + } + + Logger.LogInformation(summary); + submitDiscovery.Invoke(discoveredStores); + + if (warnings.Count > 0) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Warning, + JobHistoryId = config.JobHistoryId, + FailureMessage = summary + }; + } + + // JobResult carries only FailureMessage; Command shows it as the job message regardless of + // status, so populate it on success to surface the discovery outcome in Command. + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Success, + JobHistoryId = config.JobHistoryId, + FailureMessage = summary + }; + } + + // Single-account fallback: scan regions using the supplied credentials directly. Emits legacy + // region-only store paths so the operator supplies the Role ARN via ClientMachine on approval + // (preserving pre-cross-account behavior). + private void ScanRegionsSingleAccount(AWSCredentials credentials, List regions, + List discoveredStores, List warnings) + { + foreach (var region in regions) + { + try + { + if (RegionHasCertificates(credentials, region)) + { + Logger.LogInformation($"Discovered ACM store in region {region.SystemName}. Adding to discovered stores."); + discoveredStores.Add(region.SystemName); + } + else + { + Logger.LogDebug($"No ACM certificates found in region {region.SystemName}."); + } + } + catch (Exception ex) + { + Logger.LogWarning($"Could not check ACM in region {region.SystemName}: {ex.Message}"); + warnings.Add($"region {region.SystemName} ({ex.Message})"); + } + } + } + + // Returns true if the account/region has at least one ACM certificate. + private bool RegionHasCertificates(AWSCredentials credentials, RegionEndpoint region) + { + Logger.LogDebug($"Checking for ACM certificates in region: {region.SystemName}"); + using var acm = new AmazonCertificateManagerClient(credentials, region); + var request = new ListCertificatesRequest { MaxItems = 1 }; + var response = AsyncHelpers.RunSync(() => acm.ListCertificatesAsync(request)); + return response.CertificateSummaryList.Count > 0; + } + + // Enumerates all ACTIVE account IDs in the organization. Returns null if Organizations is + // unavailable (not a management/delegated account, AccessDenied, single-account setup, etc.), + // signalling the caller to fall back to single-account discovery. + private List TryListOrganizationAccounts(AWSCredentials credentials, RegionEndpoint baseRegion) + { + try + { + // Organizations is a global service reachable only from the partition's home region. + RegionEndpoint orgRegion = GetOrganizationsRegion(baseRegion.PartitionName); + using var org = new AmazonOrganizationsClient(credentials, orgRegion); + + var ids = new List(); + string next = null; + do + { + var req = new ListAccountsRequest { NextToken = next }; + var resp = AsyncHelpers.RunSync(() => org.ListAccountsAsync(req)); + ids.AddRange(resp.Accounts + .Where(a => a.Status == AccountStatus.ACTIVE) + .Select(a => a.Id)); + next = resp.NextToken; + } while (!string.IsNullOrEmpty(next)); + + return ids; + } + catch (Exception ex) + { + Logger.LogWarning($"organizations:ListAccounts unavailable ({ex.Message}); cross-account enumeration will be skipped."); + return null; + } + } + + // STS AssumeRole using the management credentials. The AWS SDK applies exponential backoff on + // throttling (e.g. STS rate limits) by default. + private AWSCredentials AssumeRole(AWSCredentials baseCredentials, RegionEndpoint baseRegion, + string targetRoleArn, string externalId) + { + using var sts = new AmazonSecurityTokenServiceClient(baseCredentials, baseRegion); + var req = new AssumeRoleRequest + { + RoleArn = targetRoleArn, + RoleSessionName = "KeyfactorACMDiscovery" + }; + if (!string.IsNullOrWhiteSpace(externalId)) + { + req.ExternalId = externalId; + } + + var resp = AsyncHelpers.RunSync(() => sts.AssumeRoleAsync(req)); + return new SessionAWSCredentials( + resp.Credentials.AccessKeyId, + resp.Credentials.SecretAccessKey, + resp.Credentials.SessionToken); + } + + // Builds arn:aws:iam:::role/, partition-aware for GovCloud/China. + private static string BuildMemberRoleArn(string partitionName, string accountId, string roleName) + { + string partition; + switch (partitionName) + { + case "aws-us-gov": partition = "aws-us-gov"; break; + case "aws-cn": partition = "aws-cn"; break; + default: partition = "aws"; break; + } + return $"arn:{partition}:iam::{accountId}:role/{roleName}"; + } + + // AWS Organizations has a single global endpoint per partition. + private static RegionEndpoint GetOrganizationsRegion(string partitionName) + { + switch (partitionName) + { + case "aws-us-gov": return RegionEndpoint.GetBySystemName("us-gov-west-1"); + case "aws-cn": return RegionEndpoint.GetBySystemName("cn-northwest-1"); + default: return RegionEndpoint.GetBySystemName("us-east-1"); + } + } + + // Resolves which regions to scan for one account. When the operator pinned an explicit region list + // it is used verbatim. Otherwise we call ec2:DescribeRegions with that account's own credentials and + // intersect with the partition list, so each account is scanned against its own enabled regions. + // If DescribeRegions is unavailable in the account (no ec2:DescribeRegions permission, etc.) we scan + // the full candidate list and let any disabled region fail/skip gracefully per-region. + private List ResolveAccountRegions(AWSCredentials credentials, List candidateRegions, + bool autoDetect, RegionEndpoint baseRegion, string accountLabel) + { + if (!autoDetect) + return candidateRegions; + + var enabled = GetEnabledRegions(credentials, baseRegion); + if (enabled == null) + return candidateRegions; + + var filtered = candidateRegions.Where(r => enabled.Contains(r.SystemName)).ToList(); + Logger.LogDebug($"{accountLabel}: filtered candidate regions {candidateRegions.Count} -> {filtered.Count} enabled."); + return filtered; + } + + private HashSet GetEnabledRegions(AWSCredentials credentials, RegionEndpoint baseRegion) + { + try + { + using var ec2 = new AmazonEC2Client(credentials, baseRegion); + var response = AsyncHelpers.RunSync(() => ec2.DescribeRegionsAsync(new DescribeRegionsRequest { AllRegions = false })); + return new HashSet(response.Regions.Select(r => r.RegionName), StringComparer.OrdinalIgnoreCase); + } + catch (Exception ex) + { + Logger.LogWarning($"ec2:DescribeRegions failed ({ex.Message}); will scan the full partition list and warn on any opt-in regions."); + return null; + } + } + + private List ParseRegions(string regionsConfig, string partitionName) + { + // Treat blank or "*" (spec's wildcard for "all enabled regions") as "use the full partition list". + if (string.IsNullOrWhiteSpace(regionsConfig) || regionsConfig.Trim() == "*") + return RegionEndpoint.EnumerableAllRegions + .Where(r => r.PartitionName == partitionName) + .ToList(); + + return SplitCsv(regionsConfig) + .Select(r => RegionEndpoint.GetBySystemName(r)) + .ToList(); + } + + private static IEnumerable SplitCsv(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return Enumerable.Empty(); + + return value + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => s.Length > 0); + } + + private static string FirstNonEmpty(string a, string b) + { + return string.IsNullOrWhiteSpace(a) ? b : a; + } + + private string GetJobProperty(Dictionary jobProperties, string key) + { + if (jobProperties != null && jobProperties.TryGetValue(key, out object value) && value != null) + return value.ToString(); + return null; + } + + // Case-insensitive lookup across several candidate keys (returns the first non-empty match). + // Lets a single concept be supplied either via an explicit programmatic property or via the + // standard Discovery dialog field whose JobProperties key varies by Command version. + private string GetJobPropertyMulti(Dictionary jobProperties, params string[] keys) + { + if (jobProperties == null) return null; + foreach (var key in keys) + { + var match = jobProperties + .Where(kv => kv.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) + .Select(kv => kv.Value?.ToString()) + .FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)); + if (!string.IsNullOrWhiteSpace(match)) + return match; + } + return null; + } + } +} diff --git a/aws-acm-orchestrator/Jobs/Inventory.cs b/aws-acm-orchestrator/Jobs/Inventory.cs index 2266bcb..22ce7f7 100644 --- a/aws-acm-orchestrator/Jobs/Inventory.cs +++ b/aws-acm-orchestrator/Jobs/Inventory.cs @@ -1,10 +1,16 @@ - -// Copyright 2026 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. +// Copyright 2025 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. using Amazon.CertificateManager; using Amazon.CertificateManager.Model; @@ -51,10 +57,19 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); Logger.LogTrace("Deserialized Store Properties."); + // StorePath may be a legacy region ("us-east-1") with the Role ARN in ClientMachine, or + // a self-contained "|" emitted by cross-account Discovery. Parse handles both. + var (roleArn, region) = StorePathParser.Parse( + jobConfiguration.CertificateStoreDetails.StorePath, + jobConfiguration.CertificateStoreDetails.ClientMachine); + + string storeRef = StoreRef(jobConfiguration.CertificateStoreDetails.ClientMachine, + jobConfiguration.CertificateStoreDetails.StorePath); + AuthenticationParameters authParams = new AuthenticationParameters { - RoleARN = jobConfiguration.CertificateStoreDetails.ClientMachine, - Region = jobConfiguration.CertificateStoreDetails.StorePath, + RoleARN = roleArn, + Region = region, CustomFields = customFields }; @@ -66,14 +81,16 @@ public JobResult ProcessJob(InventoryJobConfiguration jobConfiguration, SubmitIn } catch (Exception ex) { - Logger.LogError("An error occurred while trying to get AWS Credentials."); + Logger.LogError($"An error occurred while trying to get AWS Credentials for {storeRef}."); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = jobConfiguration.JobHistoryId, - FailureMessage = ex.Message + FailureMessage = $"Failed to resolve AWS credentials for {storeRef}: {ex.Message}" }; } + string authMethod = AuthDescription.DescribeMethod(customFields, roleArn); + Logger.LogInformation($"AWS credential method resolved: [{authMethod}] for {storeRef}."); Logger.LogTrace("AWS Credentials resolved. Performing Inventory."); return PerformInventory(providedCredentials, jobConfiguration, submitInventoryUpdate); @@ -84,6 +101,8 @@ internal JobResult PerformInventory(AwsExtensionCredential awsCredentials, Inven Logger.MethodEntry(); bool warningFlag = false; int totalCertificates = 0; + string storeRef = StoreRef(config.CertificateStoreDetails.ClientMachine, + config.CertificateStoreDetails.StorePath); try { List inventoryItems = new List(); @@ -136,44 +155,62 @@ internal JobResult PerformInventory(AwsExtensionCredential awsCredentials, Inven req.NextToken = AllCertificates.NextToken; } while (AllCertificates.NextToken != null); - Logger.LogDebug($"Found {totalCertificates} Total Certificates In Amazon Certificate Manager Inventory Job."); + int skipped = totalCertificates - inventoryItems.Count; + string region = awsCredentials.Region.SystemName; Logger.LogTrace($"inventoryItems Response JSON: {JsonConvert.SerializeObject(inventoryItems)}"); siu.Invoke(inventoryItems); if (warningFlag) { - Logger.LogWarning("Found Warning(s) during inventory."); + string warnSummary = $"Inventory of ACM region {region} for {storeRef} completed with warnings: found {totalCertificates} certificate(s), " + + $"reported {inventoryItems.Count} to Keyfactor Command, and skipped {skipped} that could not be retrieved from ACM " + + "(the certificate ARN and error for each skipped item are logged individually above)."; + Logger.LogWarning(warnSummary); return new JobResult { Result = OrchestratorJobStatusJobResult.Warning, JobHistoryId = config.JobHistoryId, - FailureMessage = "Check the orchestrator logs for warnings or errors that ocurred during the inventory." + FailureMessage = warnSummary }; } else { - Logger.LogTrace("No warnings found during Inventory. Reporting success."); + // JobResult exposes only FailureMessage; Command renders it as the job's message + // regardless of status, so populating it on success surfaces the outcome in Command + // (the Success result is what marks the job green — the text is informational only). + string successSummary = $"Inventory of ACM region {region} for {storeRef} succeeded: found {totalCertificates} " + + $"certificate(s) in ACM and reported all {inventoryItems.Count} to Keyfactor Command."; + Logger.LogInformation(successSummary); return new JobResult { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId, - FailureMessage = "" + FailureMessage = successSummary }; } } catch (Exception e) { - Logger.LogError($"Error ocurred in Perform Inventory: {e.Message}"); + Logger.LogError($"Error ocurred in Perform Inventory for {storeRef}: {e.Message}"); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, - FailureMessage = $"Error occurred in Perform Inventory: {e.Message}" + FailureMessage = $"Error occurred during inventory of {storeRef}: {e.Message}" }; } } + // A human-readable identifier for the certificate store this job is acting on, so success, + // warning, and failure messages surfaced in Keyfactor Command name the exact store. A store's + // identity is its ClientMachine (the Role ARN, for legacy stores) plus its StorePath (the region, + // or the self-contained "|" emitted by cross-account Discovery). + internal static string StoreRef(string clientMachine, string storePath) + { + return $"store [ClientMachine='{clientMachine}', StorePath='{storePath}']"; + } + protected virtual CurrentInventoryItem BuildInventoryItem(string alias) { try diff --git a/aws-acm-orchestrator/Jobs/Management.cs b/aws-acm-orchestrator/Jobs/Management.cs index a577685..d961bea 100644 --- a/aws-acm-orchestrator/Jobs/Management.cs +++ b/aws-acm-orchestrator/Jobs/Management.cs @@ -1,10 +1,16 @@ - -// Copyright 2026 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. +// Copyright 2025 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. using Amazon.CertificateManager; using Amazon.CertificateManager.Model; @@ -63,10 +69,19 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration) new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Populate }); Logger.LogTrace("Deserialized Store Properties."); + // StorePath may be a legacy region ("us-east-1") with the Role ARN in ClientMachine, or + // a self-contained "|" emitted by cross-account Discovery. Parse handles both. + var (roleArn, region) = StorePathParser.Parse( + jobConfiguration.CertificateStoreDetails.StorePath, + jobConfiguration.CertificateStoreDetails.ClientMachine); + + string storeRef = Inventory.StoreRef(jobConfiguration.CertificateStoreDetails.ClientMachine, + jobConfiguration.CertificateStoreDetails.StorePath); + AuthenticationParameters authParams = new AuthenticationParameters { - RoleARN = jobConfiguration.CertificateStoreDetails.ClientMachine, - Region = jobConfiguration.CertificateStoreDetails.StorePath, + RoleARN = roleArn, + Region = region, CustomFields = customFields }; @@ -78,14 +93,16 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration) } catch (Exception ex) { - Logger.LogError("An error occurred while trying to get AWS Credentials."); + Logger.LogError($"An error occurred while trying to get AWS Credentials for {storeRef}."); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = jobConfiguration.JobHistoryId, - FailureMessage = ex.Message + FailureMessage = $"Failed to resolve AWS credentials for {storeRef}: {ex.Message}" }; } + string authMethod = AuthDescription.DescribeMethod(customFields, roleArn); + Logger.LogInformation($"AWS credential method resolved: [{authMethod}] for {storeRef}."); Logger.LogTrace("AWS Credentials resolved."); // perform add or remove @@ -101,18 +118,20 @@ public JobResult ProcessJob(ManagementJobConfiguration jobConfiguration) } else { - Logger.LogError($"Unrecognized Management Operation Type: {jobConfiguration.OperationType}"); + Logger.LogError($"Unrecognized Management Operation Type: {jobConfiguration.OperationType} for {storeRef}"); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = jobConfiguration.JobHistoryId, - FailureMessage = "Invalid Management Operation" + FailureMessage = $"Invalid Management operation '{jobConfiguration.OperationType}' for {storeRef}." }; } } internal JobResult PerformAddition(AwsExtensionCredential awsCredentials, ManagementJobConfiguration config) { + string storeRef = Inventory.StoreRef(config.CertificateStoreDetails.ClientMachine, + config.CertificateStoreDetails.StorePath); try { Logger.MethodEntry(); @@ -128,23 +147,24 @@ internal JobResult PerformAddition(AwsExtensionCredential awsCredentials, Manage if (!string.IsNullOrWhiteSpace(config.JobCertificate.PrivateKeyPassword)) // This is a PFX Entry { Logger.LogTrace($"Found Private Key password."); - if (IsAcmCertificateArn(config.JobCertificate.Alias)) + if (!string.IsNullOrWhiteSpace(config.JobCertificate.Alias)) { - // Alias is an ACM certificate ARN, so this is a replace / renewal of an existing cert - Logger.LogDebug($"ACM ARN supplied as alias, validating existing cert can be renewed / replaced: {config.JobCertificate.Alias}"); + // Alias is specified, this is a replace / renewal + Logger.LogDebug($"Alias specified, validating existing cert can be renewed / replaced: {config.JobCertificate.Alias}"); // ARN Provided, Verify It is Not A PCA/Amazon Issued Cert DescribeCertificateResponse DescribeCertificateResponse = AsyncHelpers.RunSync(() => AcmClient.DescribeCertificateAsync(config.JobCertificate.Alias)); Logger.LogTrace($"DescribeCertificateResponse JSON: {JsonConvert.SerializeObject(DescribeCertificateResponse)}"); if (DescribeCertificateResponse.Certificate.Type != CertificateType.IMPORTED) { - Logger.LogError($"Non User Imported Certificate Type Found"); + Logger.LogError($"Non User Imported Certificate Type Found for {storeRef}, alias {config.JobCertificate.Alias}"); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = - "Amazon Web Services Certificate Manager only supports overwriting user-imported certificates.\"), \"Management/Add" + $"AWS Certificate Manager only supports overwriting user-imported certificates. The certificate " + + $"'{config.JobCertificate.Alias}' in {storeRef} is Amazon-issued or Private CA-issued and cannot be replaced." }; } } @@ -207,7 +227,7 @@ internal JobResult PerformAddition(AwsExtensionCredential awsCredentials, Manage CertificateChain = chainStream }; - icr.CertificateArn = IsAcmCertificateArn(config.JobCertificate.Alias) ? config.JobCertificate.Alias.Trim() : null; //If an ACM certificate ARN is provided, reimport in place (renewal/replace); otherwise import as a new certificate + icr.CertificateArn = config.JobCertificate.Alias?.Length >= 20 ? config.JobCertificate.Alias.Trim() : null; //If an arn is provided, use it, this will perform a renewal/replace Logger.LogTrace($"Certificate arn {icr.CertificateArn}"); if (icr.CertificateArn == null && acmTags != null && acmTags.Count > 0) @@ -228,67 +248,72 @@ internal JobResult PerformAddition(AwsExtensionCredential awsCredentials, Manage // Ensure 200 Response if (IcrResponse.HttpStatusCode == HttpStatusCode.OK) { - Logger.LogTrace($"Certificate Import reported success."); + // FailureMessage is JobResult's only message field; Command shows it on success + // too, so populate it to surface the outcome (Success status keeps the job green). + string successMsg = $"Certificate {(icr.CertificateArn != null ? "renewed/replaced" : "imported")} into ACM " + + $"region {awsCredentials.Region.SystemName} for {storeRef}: ARN={IcrResponse.CertificateArn}."; + Logger.LogInformation(successMsg); return new JobResult { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId, - FailureMessage = "" + FailureMessage = successMsg }; } else { - Logger.LogError($"Certificate Import reported failure."); - Logger.LogError($"Failure HTTP status code: {IcrResponse.HttpStatusCode}"); + Logger.LogError($"Certificate import into {storeRef} reported failure. HTTP status code: {IcrResponse.HttpStatusCode}"); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = - "Management/Add" + $"ACM ImportCertificate for {storeRef} returned HTTP {IcrResponse.HttpStatusCode} instead of OK." }; } } else // Non-PFX { - Logger.LogError($"Certificate did not have private key password. Only PFX certificates may be added."); + Logger.LogError($"Certificate for {storeRef} did not have a private key password. Only PFX certificates may be added."); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = - "Certificate Must be a PFX" + $"Certificate must be a PFX (with a private key) to import into {storeRef}." }; } } } catch (Exception e) { - Logger.LogError($"Error in Performing Addition: {e.Message}"); + Logger.LogError($"Error performing certificate addition to {storeRef}: {e.Message}"); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = - $"Management/Add {e.Message}" + $"Error adding certificate to {storeRef}: {e.Message}" }; } } internal JobResult PerformRemoval(AwsExtensionCredential awsCredentials, ManagementJobConfiguration config) { + string storeRef = Inventory.StoreRef(config.CertificateStoreDetails.ClientMachine, + config.CertificateStoreDetails.StorePath); try { Logger.MethodEntry(); if (string.IsNullOrEmpty(config.JobCertificate.Alias)) { - Logger.LogError("A certificate Alias containing the ARN is required in order to remove a certificate."); + Logger.LogError($"A certificate Alias containing the ARN is required in order to remove a certificate from {storeRef}."); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, - FailureMessage = "Alias is required but not present." + FailureMessage = $"A certificate Alias (the ACM ARN) is required to remove a certificate from {storeRef}, but none was provided." }; } @@ -311,59 +336,47 @@ internal JobResult PerformRemoval(AwsExtensionCredential awsCredentials, Managem Logger.LogTrace($"DeleteResponse JSON: {JsonConvert.SerializeObject(DeleteResponse)}"); if (DeleteResponse.HttpStatusCode == HttpStatusCode.OK) { - Logger.LogTrace($"Certificate Removal reported success."); + string successMsg = $"Certificate removed from ACM region {awsCredentials.Region.SystemName} for {storeRef}: ARN={config.JobCertificate.Alias}."; + Logger.LogInformation(successMsg); return new JobResult { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId, - FailureMessage = "" + FailureMessage = successMsg }; } else { - Logger.LogError($"Certificate Removal reported failure."); - Logger.LogError($"Failure HTTP status code - {DeleteResponse.HttpStatusCode}"); + Logger.LogError($"Certificate removal from {storeRef} (ARN={config.JobCertificate.Alias}) reported failure. HTTP status code: {DeleteResponse.HttpStatusCode}"); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = - "Management/Remove" + $"ACM DeleteCertificate for {storeRef} (ARN={config.JobCertificate.Alias}) returned HTTP {DeleteResponse.HttpStatusCode} instead of OK." }; } } } catch (Exception e) { - Logger.LogError($"Error in Perform Removal: {e.Message}"); + Logger.LogError($"Error performing certificate removal from {storeRef}: {e.Message}"); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = - $"Management/Remove: {e.Message}" + $"Error removing certificate from {storeRef}: {e.Message}" }; } } - internal static MemoryStream GetChain(Pkcs12Store store, string alias) + private static MemoryStream GetChain(Pkcs12Store store, string alias) { string ccs = ""; X509CertificateEntry[] chain = store.GetCertificateChain(alias); - // BouncyCastle returns the chain with the leaf/end-entity certificate as element [0], - // followed by any intermediates (and root). The leaf is already sent separately as the - // Certificate body of the ImportCertificateRequest, so it must NOT be repeated here; - // ACM's CertificateChain is expected to contain only the intermediate (and root) certs. - // Including the leaf caused it to appear twice within the published certificate's chain. - if (chain == null || chain.Length <= 1) - { - // Only the leaf is present (no intermediates) - omit the chain entirely rather than - // sending an empty value, which ACM may reject as an unparseable certificate chain. - return null; - } - foreach (X509CertificateEntry chainEntry in chain.Skip(1)) { ccs += certStart + _pemify(Convert.ToBase64String(chainEntry.Certificate.GetEncoded())) + certEnd + "\n"; @@ -380,22 +393,6 @@ private static MemoryStream CertStringToStream(string certString) return new MemoryStream(certBytes); } - /// - /// Determines whether the supplied alias is an AWS Certificate Manager certificate ARN - /// (e.g. arn:aws:acm:<region>:<account>:certificate/<id>). When it is, the certificate - /// already exists in ACM and an Add job should reimport in place (renewal/replace); - /// otherwise the certificate is imported as a new one and ACM assigns a fresh ARN. - /// Replaces an earlier alias-length heuristic that could misclassify a long friendly alias. - /// - internal static bool IsAcmCertificateArn(string alias) - { - if (string.IsNullOrWhiteSpace(alias)) return false; - - string trimmed = alias.Trim(); - return trimmed.StartsWith("arn:aws:acm:", StringComparison.OrdinalIgnoreCase) - && trimmed.Contains(":certificate/"); - } - private List ParseACMTags(Dictionary jobProperties) { List acmTags = new List(); diff --git a/aws-acm-orchestrator/Jobs/StorePathParser.cs b/aws-acm-orchestrator/Jobs/StorePathParser.cs new file mode 100644 index 0000000..c43b5a5 --- /dev/null +++ b/aws-acm-orchestrator/Jobs/StorePathParser.cs @@ -0,0 +1,66 @@ +// Copyright 2025 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace Keyfactor.Extensions.Orchestrator.Aws.Acm.Jobs +{ + /// + /// A certificate store's identity is the AWS Role ARN to assume (which embeds the target + /// account) plus the AWS Region. Historically these lived in two separate fields: + /// ClientMachine held the Role ARN and StorePath held the Region. + /// + /// Cross-account Discovery can only return a list of StorePath strings (it has no channel to + /// set ClientMachine per discovered store), so discovered stores fold both values into a single + /// self-contained StorePath of the form "|". '|' cannot appear in an ARN or a + /// region name, so it is an unambiguous separator. + /// + /// This helper parses both shapes so legacy (manually-created) stores and new (discovered) + /// stores resolve through one code path. + /// + internal static class StorePathParser + { + internal const char Separator = '|'; + + /// + /// Resolves the Role ARN and Region for a store. + /// New/discovered stores: StorePath = "|" — both come from StorePath. + /// Legacy stores: StorePath = "" and the Role ARN comes from ClientMachine. + /// + public static (string RoleArn, string Region) Parse(string storePath, string clientMachine) + { + if (!string.IsNullOrWhiteSpace(storePath) && storePath.IndexOf(Separator) >= 0) + { + string[] parts = storePath.Split(new[] { Separator }, 2); + string role = parts[0].Trim(); + string region = parts.Length > 1 ? parts[1].Trim() : string.Empty; + + // An empty role segment falls back to ClientMachine so a hand-edited combined path + // like "|us-east-1" still works. + return (string.IsNullOrWhiteSpace(role) ? clientMachine : role, region); + } + + // Legacy shape: region in StorePath, role in ClientMachine. + return (clientMachine, storePath?.Trim()); + } + + /// + /// Builds the self-contained StorePath emitted by Discovery for a discovered store. + /// + public static string Build(string roleArn, string region) + { + return $"{roleArn}{Separator}{region}"; + } + } +} diff --git a/aws-acm-orchestrator/manifest.json b/aws-acm-orchestrator/manifest.json index 764fcad..2eebcd0 100644 --- a/aws-acm-orchestrator/manifest.json +++ b/aws-acm-orchestrator/manifest.json @@ -8,6 +8,10 @@ "CertStores.AWS-ACM-v3.Management": { "assemblypath": "Keyfactor.Extensions.Orchestrator.Aws.Acm.dll", "TypeFullName": "Keyfactor.Extensions.Orchestrator.Aws.Acm.Jobs.Management" + }, + "CertStores.AWS-ACM-v3.Discovery": { + "assemblypath": "Keyfactor.Extensions.Orchestrator.Aws.Acm.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.Aws.Acm.Jobs.Discovery" } } } diff --git a/docsource/content.md b/docsource/content.md index be6a41f..10dca4c 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -1,5 +1,13 @@ ## Overview +> [!WARNING] +> +> **The `AWS-ACM-v3` store type identifies each certificate store by a single AWS `Region` in the `Store Path` field, with the full Role ARN in `Client Machine`.** +> +> **New with cross-account Discovery:** `Store Path` now also accepts a **self-contained** value of the form **`|`** — for example `arn:aws:iam::123456789012:role/KeyfactorACMDiscoveryRole|us-east-1` — which carries **both** the Role ARN to assume **and** the Region in a single field. Certificate stores created by cross-account Discovery use this combined format, and for those stores the `Client Machine` field is informational only. Legacy region-only `Store Path` values (with the Role ARN in `Client Machine`) continue to work. +> +> **Migrating from `AWS-ACM`, `AwsCerManO`, or `AwsCerManA`:** those older store types are **not** compatible and must be recreated as `AWS-ACM-v3`. + AWS Certificate Manager is a service that lets you easily provision, manage, and deploy public and private Secure Sockets Layer/Transport Layer Security (SSL/TLS) certificates for use with AWS services and your internal connected resources. SSL/TLS certificates are used to secure network communications and establish the identity of websites over the Internet as well as resources on private networks. diff --git a/integration-manifest.json b/integration-manifest.json index df2d856..67565a6 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -23,7 +23,7 @@ "SupportedOperations": { "Add": true, "Create": false, - "Discovery": false, + "Discovery": true, "Enrollment": false, "Remove": true }, @@ -147,6 +147,16 @@ "Required": false, "IsPAMEligible": false, "Description": "An optional parameter sts:ExternalId to pass with Assume Role calls" + }, + { + "Name": "DiscoveryRoleName", + "DisplayName": "Discovery Role Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "KeyfactorACMDiscoveryRole", + "Required": false, + "IsPAMEligible": false, + "Description": "The IAM role name that exists in each target account. Used during cross-account Discovery to construct the Role ARN for cross-account access (arn:aws:iam:::role/). During a Discovery job this is supplied via the 'File name patterns to match' dialog field; this store property documents and defaults the value." } ], "EntryParameters": [ @@ -174,7 +184,7 @@ "BlueprintAllowed": true, "CustomAliasAllowed": "Optional", "ClientMachineDescription": "This is a full AWS ARN specifying a Role. This is the Role that will be assumed in any Auth scenario performing Assume Role. This will dictate what certificates are usable by the orchestrator. A preceding [profile] name should be included if a Credential Profile is to be used in Default Sdk Auth.", - "StorePathDescription": "A single specified AWS Region the store will operate in. Additional regions should get their own store defined." + "StorePathDescription": "The AWS Region the store operates in (e.g. us-east-1). Stores created by cross-account Discovery instead use a self-contained path of the form '|' (e.g. 'arn:aws:iam::123456789012:role/KeyfactorACMDiscoveryRole|us-east-1') which carries both the Role ARN to assume and the Region; for those stores the Client Machine field is informational only. Additional regions should get their own store defined." } ] } diff --git a/scripts/store_types/bash/curl_create_store_types.sh b/scripts/store_types/bash/curl_create_store_types.sh index 3bf2861..ad15eb6 100755 --- a/scripts/store_types/bash/curl_create_store_types.sh +++ b/scripts/store_types/bash/curl_create_store_types.sh @@ -80,7 +80,7 @@ create_store_type "AWS-ACM-v3" '{ "SupportedOperations": { "Add": true, "Create": false, - "Discovery": false, + "Discovery": true, "Enrollment": false, "Remove": true }, @@ -192,6 +192,15 @@ create_store_type "AWS-ACM-v3" '{ "DefaultValue": "", "Required": false, "IsPAMEligible": false + }, + { + "Name": "DiscoveryRoleName", + "DisplayName": "Discovery Role Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "KeyfactorACMDiscoveryRole", + "Required": false, + "IsPAMEligible": false } ], "EntryParameters": [ @@ -218,7 +227,7 @@ create_store_type "AWS-ACM-v3" '{ "PowerShell": false, "BlueprintAllowed": true, "CustomAliasAllowed": "Optional", - "StorePathDescription": "A single specified AWS Region the store will operate in. Additional regions should get their own store defined." + "StorePathDescription": "The AWS Region the store operates in (e.g. us-east-1). Stores created by cross-account Discovery instead use a self-contained path of the form '|' (e.g. 'arn:aws:iam::123456789012:role/KeyfactorACMDiscoveryRole|us-east-1') which carries both the Role ARN to assume and the Region; for those stores the Client Machine field is informational only. Additional regions should get their own store defined." }' diff --git a/scripts/store_types/powershell/restmethod_create_store_types.ps1 b/scripts/store_types/powershell/restmethod_create_store_types.ps1 index 1987078..3519c66 100644 --- a/scripts/store_types/powershell/restmethod_create_store_types.ps1 +++ b/scripts/store_types/powershell/restmethod_create_store_types.ps1 @@ -73,7 +73,7 @@ New-StoreType "AWS-ACM-v3" @' "SupportedOperations": { "Add": true, "Create": false, - "Discovery": false, + "Discovery": true, "Enrollment": false, "Remove": true }, @@ -185,6 +185,15 @@ New-StoreType "AWS-ACM-v3" @' "DefaultValue": "", "Required": false, "IsPAMEligible": false + }, + { + "Name": "DiscoveryRoleName", + "DisplayName": "Discovery Role Name", + "Type": "String", + "DependsOn": "", + "DefaultValue": "KeyfactorACMDiscoveryRole", + "Required": false, + "IsPAMEligible": false } ], "EntryParameters": [ @@ -211,7 +220,7 @@ New-StoreType "AWS-ACM-v3" @' "PowerShell": false, "BlueprintAllowed": true, "CustomAliasAllowed": "Optional", - "StorePathDescription": "A single specified AWS Region the store will operate in. Additional regions should get their own store defined." + "StorePathDescription": "The AWS Region the store operates in (e.g. us-east-1). Stores created by cross-account Discovery instead use a self-contained path of the form '|' (e.g. 'arn:aws:iam::123456789012:role/KeyfactorACMDiscoveryRole|us-east-1') which carries both the Role ARN to assume and the Region; for those stores the Client Machine field is informational only. Additional regions should get their own store defined." } '@ From 460ce9f0783d6775ef9acaf246539b46bc71ed8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Aug 2026 19:28:08 +0000 Subject: [PATCH 2/6] docs: auto-generate README and documentation [skip ci] --- README.md | 148 ++++++----------- .../AWS-ACM-v3-advanced-store-type-dialog.svg | 67 ++++++++ .../AWS-ACM-v3-basic-store-type-dialog.svg | 84 ++++++++++ ...stom-field-DefaultSdkAssumeRole-dialog.svg | 55 ++++++ ...dkAssumeRole-validation-options-dialog.svg | 39 +++++ ...-custom-field-DiscoveryRoleName-dialog.svg | 49 ++++++ ...veryRoleName-validation-options-dialog.svg | 39 +++++ ...-ACM-v3-custom-field-ExternalId-dialog.svg | 49 ++++++ ...d-ExternalId-validation-options-dialog.svg | 39 +++++ ...3-custom-field-IAMUserAccessKey-dialog.svg | 49 ++++++ ...serAccessKey-validation-options-dialog.svg | 39 +++++ ...ustom-field-IAMUserAccessSecret-dialog.svg | 49 ++++++ ...AccessSecret-validation-options-dialog.svg | 39 +++++ ...M-v3-custom-field-OAuthClientId-dialog.svg | 49 ++++++ ...AuthClientId-validation-options-dialog.svg | 39 +++++ ...-custom-field-OAuthClientSecret-dialog.svg | 49 ++++++ ...ClientSecret-validation-options-dialog.svg | 39 +++++ ...-v3-custom-field-OAuthGrantType-dialog.svg | 50 ++++++ ...uthGrantType-validation-options-dialog.svg | 39 +++++ ...-ACM-v3-custom-field-OAuthScope-dialog.svg | 50 ++++++ ...d-OAuthScope-validation-options-dialog.svg | 39 +++++ ...WS-ACM-v3-custom-field-OAuthUrl-dialog.svg | 50 ++++++ ...eld-OAuthUrl-validation-options-dialog.svg | 39 +++++ ...-custom-field-UseDefaultSdkAuth-dialog.svg | 54 ++++++ ...faultSdkAuth-validation-options-dialog.svg | 39 +++++ .../AWS-ACM-v3-custom-field-UseIAM-dialog.svg | 54 ++++++ ...field-UseIAM-validation-options-dialog.svg | 39 +++++ ...WS-ACM-v3-custom-field-UseOAuth-dialog.svg | 54 ++++++ ...eld-UseOAuth-validation-options-dialog.svg | 39 +++++ ...ACM-v3-custom-fields-store-type-dialog.svg | 157 ++++++++++++++++++ ...ype-dialog-ACM Tags-validation-options.svg | 68 ++++++++ ...-parameters-store-type-dialog-ACM Tags.svg | 51 ++++++ ...-v3-entry-parameters-store-type-dialog.svg | 54 ++++++ .../bash/curl_create_store_types.sh | 130 +++++---------- .../bash/kfutil_create_store_types.sh | 31 +--- .../powershell/kfutil_create_store_types.ps1 | 31 +--- .../restmethod_create_store_types.ps1 | 119 +++++-------- 37 files changed, 1788 insertions(+), 320 deletions(-) create mode 100644 docsource/images/AWS-ACM-v3-advanced-store-type-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-basic-store-type-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-ExternalId-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-ExternalId-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthScope-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthScope-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-UseIAM-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-UseIAM-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-UseOAuth-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-field-UseOAuth-validation-options-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-custom-fields-store-type-dialog.svg create mode 100644 docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags-validation-options.svg create mode 100644 docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags.svg create mode 100644 docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog.svg diff --git a/README.md b/README.md index c4a3e1e..0115507 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@

Integration Status: production -Release -Issues -GitHub Downloads (all assets, all releases) +Release +Issues +GitHub Downloads (all assets, all releases)

@@ -58,13 +58,12 @@ However, while the modification/addition of ACM tags is not supported, all exist - [How AWS works in this extension (aws-auth-library)](https://github.com/Keyfactor/aws-auth-library) - [AWS Region Codes](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) - - ## Compatibility This integration is compatible with Keyfactor Universal Orchestrator version 10.1 and later. ## Support + The AWS Certificate Manager (ACM) Universal Orchestrator extension is supported by Keyfactor. If you require support for any issues or have feature request, please open a support ticket by either contacting your Keyfactor representative or via the Keyfactor Support Portal at https://support.keyfactor.com. > If you want to contribute bug fixes or additional enhancements, use the **[Pull requests](../../pulls)** tab. @@ -73,7 +72,6 @@ The AWS Certificate Manager (ACM) Universal Orchestrator extension is supported Before installing the AWS Certificate Manager (ACM) Universal Orchestrator extension, we recommend that you install [kfutil](https://github.com/Keyfactor/kfutil). Kfutil is a command-line tool that simplifies the process of creating store types, installing extensions, and instantiating certificate stores in Keyfactor Command. - ### Migrate existing ACM stores to the new type (AWS Certificate Manager v3) Field usage has changed in v3, notably: @@ -148,13 +146,10 @@ Select the `Use IAM` option for a certificate store to use an IAM User credentia - ## AWS-ACM-v3 Certificate Store Type To use the AWS Certificate Manager (ACM) Universal Orchestrator extension, you **must** create the AWS-ACM-v3 Certificate Store Type. This only needs to happen _once_ per Keyfactor Command instance. - - The AWS Certificate Manager v3 Store Type provides enhanced authentication options for managing certificates in ACM. Each defined Certificate Store of this type targets a specific AWS Region with a specific Destination account in mind. Therefore, each Certificate Store instance is intended to represent a single Role's certificates in a single AWS Region. @@ -162,6 +157,7 @@ Therefore, each Certificate Store instance is intended to represent a single Rol Some authentication configurations do not adhere strictly to this, so when using the various methods offered in the Default SDK auth option, a full understanding of how permissions work in AWS is recommended. In most scenarios using the Default SDK option, the Assume Role flag should also be set to avoid confusion, and use the Role ARN in the `Client Machine` field as the Destination account. + The latest version of the Store Type supporting ACM (AWS Certificate Manager) is `AWS-ACM-v3`. Previous store types are no longer supported and should be migrated to the new Store Type definition. When migrating to the `AWS-ACM-v3` type please note that field usage has changed and does not map over directly. @@ -170,10 +166,6 @@ When migrating to the `AWS-ACM-v3` type please note that field usage has changed > When creating Certificate Stores, all available Secret type fields need to have a value set for them, even if that is "No Value". > Failing to set these Secret fields, even when not in use, causes errors that may require database access to fix. - - - - #### AWS Certificate Manager v3 Requirements Configuring authentication with AWS requires understanding how the authentication flow works. @@ -195,23 +187,22 @@ Inventory required actions: "acm:ImportCertificate" ~~~ - - #### Supported Operations -| Operation | Is Supported | -|--------------|------------------------------------------------------------------------------------------------------------------------| -| Add | ✅ Checked | -| Remove | ✅ Checked | -| Discovery | ✅ Checked | +| Operation | Is Supported | +|--------------|--------------| +| Add | ✅ Checked | +| Remove | ✅ Checked | +| Discovery | ✅ Checked | | Reenrollment | 🔲 Unchecked | -| Create | 🔲 Unchecked | +| Create | 🔲 Unchecked | #### Store Type Creation ##### Using kfutil: `kfutil` is a custom CLI for the Keyfactor Command API and can be used to create certificate store types. For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out the [docs](https://github.com/Keyfactor/kfutil?tab=readme-ov-file#quickstart) +

Click to expand AWS-ACM-v3 kfutil details ##### Using online definition from GitHub: @@ -230,10 +221,10 @@ For more information on [kfutil](https://github.com/Keyfactor/kfutil) check out ```
- #### Manual Creation Below are instructions on how to create the AWS-ACM-v3 store type manually in the Keyfactor Command Portal +
Click to expand manual AWS-ACM-v3 details Create a store type called `AWS-ACM-v3` with the attributes in the tables below: @@ -244,11 +235,11 @@ the Keyfactor Command Portal | Name | AWS Certificate Manager v3 | Display name for the store type (may be customized) | | Short Name | AWS-ACM-v3 | Short display name for the store type | | Capability | AWS-ACM-v3 | Store type name orchestrator will register with. Check the box to allow entry of value | - | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add | - | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove | - | Supports Discovery | ✅ Checked | Check the box. Indicates that the Store Type supports Discovery | - | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | - | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | + | Supports Add | ✅ Checked | Indicates that the Store Type supports Management Add | + | Supports Remove | ✅ Checked | Indicates that the Store Type supports Management Remove | + | Supports Discovery | ✅ Checked | Indicates that the Store Type supports Discovery | + | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | + | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | | Needs Server | 🔲 Unchecked | Determines if a target server name is required when creating store | | Blueprint Allowed | ✅ Checked | Determines if store type may be included in an Orchestrator blueprint | | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell | @@ -257,18 +248,18 @@ the Keyfactor Command Portal The Basic tab should look like this: - ![AWS-ACM-v3 Basic Tab](docsource/images/AWS-ACM-v3-basic-store-type-dialog.png) + ![AWS-ACM-v3 Basic Tab](docsource/images/AWS-ACM-v3-basic-store-type-dialog.svg) ##### Advanced Tab | Attribute | Value | Description | | --------- | ----- | ----- | | Supports Custom Alias | Optional | Determines if an individual entry within a store can have a custom Alias. | - | Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. | + | Private Key Handling | Required | This determines if Keyfactor can send the private key associated with a certificate to the store. | | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) | The Advanced tab should look like this: - ![AWS-ACM-v3 Advanced Tab](docsource/images/AWS-ACM-v3-advanced-store-type-dialog.png) + ![AWS-ACM-v3 Advanced Tab](docsource/images/AWS-ACM-v3-advanced-store-type-dialog.svg) > For Keyfactor **Command versions 24.4 and later**, a Certificate Format dropdown is available with PFX and PEM options. Ensure that **PFX** is selected, as this determines the format of new and renewed certificates sent to the Orchestrator during a Management job. Currently, all Keyfactor-supported Orchestrator extensions support only PFX. @@ -293,113 +284,84 @@ the Keyfactor Command Portal The Custom Fields tab should look like this: - ![AWS-ACM-v3 Custom Fields Tab](docsource/images/AWS-ACM-v3-custom-fields-store-type-dialog.png) - + ![AWS-ACM-v3 Custom Fields Tab](docsource/images/AWS-ACM-v3-custom-fields-store-type-dialog.svg) ###### Use Default SDK Auth A switch to enable the store to use Default SDK credentials - ![AWS-ACM-v3 Custom Field - UseDefaultSdkAuth](docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-dialog.png) - ![AWS-ACM-v3 Custom Field - UseDefaultSdkAuth](docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - UseDefaultSdkAuth](docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-dialog.svg) ###### Assume new Role using Default SDK Auth A switch to enable the store to assume a new Role when using Default SDK credentials - ![AWS-ACM-v3 Custom Field - DefaultSdkAssumeRole](docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-dialog.png) - ![AWS-ACM-v3 Custom Field - DefaultSdkAssumeRole](docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - DefaultSdkAssumeRole](docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-dialog.svg) ###### Use OAuth 2.0 Provider A switch to enable the store to use an OAuth provider workflow to authenticate with AWS - ![AWS-ACM-v3 Custom Field - UseOAuth](docsource/images/AWS-ACM-v3-custom-field-UseOAuth-dialog.png) - ![AWS-ACM-v3 Custom Field - UseOAuth](docsource/images/AWS-ACM-v3-custom-field-UseOAuth-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - UseOAuth](docsource/images/AWS-ACM-v3-custom-field-UseOAuth-dialog.svg) ###### OAuth Scope This is the OAuth Scope needed for Okta OAuth, defined in Okta - ![AWS-ACM-v3 Custom Field - OAuthScope](docsource/images/AWS-ACM-v3-custom-field-OAuthScope-dialog.png) - ![AWS-ACM-v3 Custom Field - OAuthScope](docsource/images/AWS-ACM-v3-custom-field-OAuthScope-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - OAuthScope](docsource/images/AWS-ACM-v3-custom-field-OAuthScope-dialog.svg) ###### OAuth Grant Type In OAuth 2.0, the term 'grant type' refers to the way an application gets an access token. In Okta this is `client_credentials` - ![AWS-ACM-v3 Custom Field - OAuthGrantType](docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-dialog.png) - ![AWS-ACM-v3 Custom Field - OAuthGrantType](docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - OAuthGrantType](docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-dialog.svg) ###### OAuth Url An optional parameter sts:ExternalId to pass with Assume Role calls - ![AWS-ACM-v3 Custom Field - OAuthUrl](docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-dialog.png) - ![AWS-ACM-v3 Custom Field - OAuthUrl](docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - OAuthUrl](docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-dialog.svg) ###### OAuth Client ID The Client ID for OAuth. - ![AWS-ACM-v3 Custom Field - OAuthClientId](docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-dialog.png) - ![AWS-ACM-v3 Custom Field - OAuthClientId](docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - OAuthClientId](docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-dialog.svg) ###### OAuth Client Secret The Client Secret for OAuth. - ![AWS-ACM-v3 Custom Field - OAuthClientSecret](docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-dialog.png) - ![AWS-ACM-v3 Custom Field - OAuthClientSecret](docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - OAuthClientSecret](docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-dialog.svg) ###### Use IAM User Auth A switch to enable the store to use IAM User auth to assume a role when authenticating with AWS - ![AWS-ACM-v3 Custom Field - UseIAM](docsource/images/AWS-ACM-v3-custom-field-UseIAM-dialog.png) - ![AWS-ACM-v3 Custom Field - UseIAM](docsource/images/AWS-ACM-v3-custom-field-UseIAM-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - UseIAM](docsource/images/AWS-ACM-v3-custom-field-UseIAM-dialog.svg) ###### IAM User Access Key The AWS Access Key for an IAM User - ![AWS-ACM-v3 Custom Field - IAMUserAccessKey](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-dialog.png) - ![AWS-ACM-v3 Custom Field - IAMUserAccessKey](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - IAMUserAccessKey](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-dialog.svg) ###### IAM User Access Secret The AWS Access Secret for an IAM User. - ![AWS-ACM-v3 Custom Field - IAMUserAccessSecret](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-dialog.png) - ![AWS-ACM-v3 Custom Field - IAMUserAccessSecret](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - IAMUserAccessSecret](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-dialog.svg) ###### sts:ExternalId An optional parameter sts:ExternalId to pass with Assume Role calls - ![AWS-ACM-v3 Custom Field - ExternalId](docsource/images/AWS-ACM-v3-custom-field-ExternalId-dialog.png) - ![AWS-ACM-v3 Custom Field - ExternalId](docsource/images/AWS-ACM-v3-custom-field-ExternalId-validation-options-dialog.png) - + ![AWS-ACM-v3 Custom Field - ExternalId](docsource/images/AWS-ACM-v3-custom-field-ExternalId-dialog.svg) ###### Discovery Role Name The IAM role name that exists in each target account. Used during cross-account Discovery to construct the Role ARN for cross-account access (arn:aws:iam:::role/). During a Discovery job this is supplied via the 'File name patterns to match' dialog field; this store property documents and defaults the value. - ![AWS-ACM-v3 Custom Field - DiscoveryRoleName](docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-dialog.png) - ![AWS-ACM-v3 Custom Field - DiscoveryRoleName](docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-validation-options-dialog.png) - - - + ![AWS-ACM-v3 Custom Field - DiscoveryRoleName](docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-dialog.svg) ##### Entry Parameters Tab @@ -410,15 +372,11 @@ the Keyfactor Command Portal The Entry Parameters tab should look like this: - ![AWS-ACM-v3 Entry Parameters Tab](docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog.png) - - + ![AWS-ACM-v3 Entry Parameters Tab](docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog.svg) ##### ACM Tags The optional ACM tags that should be assigned to the certificate. Multiple name/value pairs may be entered in the format of `Name1=Value1,Name2=Value2,...,NameN=ValueN` - ![AWS-ACM-v3 Entry Parameter - ACM Tags](docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags.png) - ![AWS-ACM-v3 Entry Parameter - ACM Tags](docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags-validation-options.png) - + ![AWS-ACM-v3 Entry Parameter - ACM Tags](docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags.svg)
@@ -427,12 +385,15 @@ the Keyfactor Command Portal 1. **Download the latest AWS Certificate Manager (ACM) Universal Orchestrator extension from GitHub.** - Navigate to the [AWS Certificate Manager (ACM) Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/aws-acm-orchestrator-dev/releases/latest). Refer to the compatibility matrix below to determine the asset should be downloaded. Then, click the corresponding asset to download the zip archive. + Navigate to the [AWS Certificate Manager (ACM) Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/aws-orchestrator/releases/latest). Refer to the compatibility matrix below to determine which asset should be downloaded. Then, click the corresponding asset to download the zip archive. - | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `aws-acm-orchestrator-dev` .NET version to download | + | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `aws-orchestrator` .NET version to download | | --------- | ----------- | ----------- | ----------- | + | Older than `11.0.0` | | | `net6.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` | | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | - | `11.6` _and_ newer | `net8.0` | | `net8.0` | + | `11.6` _and_ newer | `net8.0` | | `net8.0` | Unzip the archive containing extension assemblies to a known location. @@ -445,30 +406,25 @@ the Keyfactor Command Portal 3. **Create a new directory for the AWS Certificate Manager (ACM) Universal Orchestrator extension inside the extensions directory.** - Create a new directory called `aws-acm-orchestrator-dev`. + Create a new directory called `aws-orchestrator`. > The directory name does not need to match any names used elsewhere; it just has to be unique within the extensions directory. -4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `aws-acm-orchestrator-dev` directory.** +4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `aws-orchestrator` directory.** 5. **Restart the Universal Orchestrator service.** Refer to [Starting/Restarting the Universal Orchestrator service](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/StarttheService.htm). - 6. **(optional) PAM Integration** The AWS Certificate Manager (ACM) Universal Orchestrator extension is compatible with all supported Keyfactor PAM extensions to resolve PAM-eligible secrets. PAM extensions running on Universal Orchestrators enable secure retrieval of secrets from a connected PAM provider. To configure a PAM provider, [reference the Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam) to select an extension and follow the associated instructions to install it on the Universal Orchestrator (remote). - > The above installation steps can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/CustomExtensions.htm?Highlight=extensions). - - ## Defining Certificate Stores - The latest version of the Store Type supporting ACM (AWS Certificate Manager) is `AWS-ACM-v3`. Previous store types are no longer supported and should be migrated to the new Store Type definition. When migrating to the `AWS-ACM-v3` type please note that field usage has changed and does not map over directly. @@ -477,7 +433,6 @@ When migrating to the `AWS-ACM-v3` type please note that field usage has changed > When creating Certificate Stores, all available Secret type fields need to have a value set for them, even if that is "No Value". > Failing to set these Secret fields, even when not in use, causes errors that may require database access to fix. - ### Store Creation #### Manually with the Command UI @@ -492,8 +447,8 @@ When migrating to the `AWS-ACM-v3` type please note that field usage has changed Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. - | Attribute | Description | - | --------- |---------------------------------------------------------| + | Attribute | Description | + | --------- | ----------- | | Category | Select "AWS Certificate Manager v3" or the customized certificate store name from the previous step. | | Container | Optional container to associate certificate store with. | | Client Machine | This is a full AWS ARN specifying a Role. This is the Role that will be assumed in any Auth scenario performing Assume Role. This will dictate what certificates are usable by the orchestrator. A preceding [profile] name should be included if a Credential Profile is to be used in Default Sdk Auth. | @@ -515,8 +470,6 @@ When migrating to the `AWS-ACM-v3` type please note that field usage has changed - - #### Using kfutil CLI
Click to expand details @@ -559,7 +512,6 @@ When migrating to the `AWS-ACM-v3` type please note that field usage has changed
- #### PAM Provider Eligible Fields
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator @@ -577,17 +529,13 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov
- > The content in this section can be supplemented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store). - - - ## License Apache License 2.0, see [LICENSE](LICENSE). ## Related Integrations -See all [Keyfactor Universal Orchestrator extensions](https://github.com/orgs/Keyfactor/repositories?q=orchestrator). \ No newline at end of file +See all [Keyfactor Universal Orchestrator extensions](https://github.com/orgs/Keyfactor/repositories?q=orchestrator). diff --git a/docsource/images/AWS-ACM-v3-advanced-store-type-dialog.svg b/docsource/images/AWS-ACM-v3-advanced-store-type-dialog.svg new file mode 100644 index 0000000..cc5f3ab --- /dev/null +++ b/docsource/images/AWS-ACM-v3-advanced-store-type-dialog.svg @@ -0,0 +1,67 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + + Custom Fields + Entry Parameters + + + + + Store Path Type + + + + Freeform + + Fixed + + Multiple Choice + + + + + Other Settings + + Supports Custom Alias + + Forbidden + + + Optional + + Required + Private Key Handling + + Forbidden + + Optional + + + Required + PFX Password Style + + + Default + + Custom + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-basic-store-type-dialog.svg b/docsource/images/AWS-ACM-v3-basic-store-type-dialog.svg new file mode 100644 index 0000000..bc90111 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-basic-store-type-dialog.svg @@ -0,0 +1,84 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + + Advanced + Custom Fields + Entry Parameters + + + + + Details + + Name + + AWS Certificate Manager v3 + Short Name + + AWS-ACM-v3 + Custom Capability + + + Custom Capability + + + + Supported Job Types + + + + Inventory + + + Add + + + Remove + + Create + + + Discovery + + ODKG + + + + General Settings + + + Needs Server + + + Blueprint Allowed + + Uses PowerShell + + + + Password Settings + + + Requires Store Password + + Supports Entry Password + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-dialog.svg new file mode 100644 index 0000000..efa7a36 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-dialog.svg @@ -0,0 +1,55 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + DefaultSdkAssumeRole + Display Name + + Assume new Role using Default SDK Auth + Type + + Bool + + Default Value + + True + + + False + + Not Set + Depends On + + + + Use Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-dialog.svg new file mode 100644 index 0000000..0501570 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + DiscoveryRoleName + Display Name + + Discovery Role Name + Type + + String + + Default Value + + KeyfactorACMDiscoveryRole + Depends On + + + Use Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-ExternalId-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-ExternalId-dialog.svg new file mode 100644 index 0000000..f4430a9 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-ExternalId-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + ExternalId + Display Name + + sts:ExternalId + Type + + String + + Default Value + + + Depends On + + + Use Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-ExternalId-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-ExternalId-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-ExternalId-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-dialog.svg new file mode 100644 index 0000000..2c7e9b1 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + IAMUserAccessKey + Display Name + + IAM User Access Key + Type + + Secret + + Default Value + + + Depends On + + + Use Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-dialog.svg new file mode 100644 index 0000000..95d1ebe --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + IAMUserAccessSecret + Display Name + + IAM User Access Secret + Type + + Secret + + Default Value + + + Depends On + + + Use Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-dialog.svg new file mode 100644 index 0000000..9b2d27b --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + OAuthClientId + Display Name + + OAuth Client ID + Type + + Secret + + Default Value + + + Depends On + + + Use Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-dialog.svg new file mode 100644 index 0000000..58727c8 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-dialog.svg @@ -0,0 +1,49 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + OAuthClientSecret + Display Name + + OAuth Client Secret + Type + + Secret + + Default Value + + + Depends On + + + Use Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-dialog.svg new file mode 100644 index 0000000..9b8aa94 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-dialog.svg @@ -0,0 +1,50 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + OAuthGrantType + Display Name + + OAuth Grant Type + Type + + String + + Default Value + + client_credentials + Depends On + + + + Use OAuth 2.0 Provider + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthScope-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthScope-dialog.svg new file mode 100644 index 0000000..71d8a33 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthScope-dialog.svg @@ -0,0 +1,50 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + OAuthScope + Display Name + + OAuth Scope + Type + + String + + Default Value + + + Depends On + + + + Use OAuth 2.0 Provider + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthScope-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthScope-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthScope-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-dialog.svg new file mode 100644 index 0000000..9b81e1e --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-dialog.svg @@ -0,0 +1,50 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + OAuthUrl + Display Name + + OAuth Url + Type + + String + + Default Value + + https://***/oauth2/default/v1/token + Depends On + + + + Use OAuth 2.0 Provider + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-validation-options-dialog.svg new file mode 100644 index 0000000..22f8bbd --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-dialog.svg new file mode 100644 index 0000000..d95def0 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + UseDefaultSdkAuth + Display Name + + Use Default SDK Auth + Type + + Bool + + Default Value + + True + + + False + + Not Set + Depends On + + + Assume new Role using Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-validation-options-dialog.svg new file mode 100644 index 0000000..7993c23 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-UseIAM-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-UseIAM-dialog.svg new file mode 100644 index 0000000..6628f5f --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-UseIAM-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + UseIAM + Display Name + + Use IAM User Auth + Type + + Bool + + Default Value + + True + + + False + + Not Set + Depends On + + + Use Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-UseIAM-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-UseIAM-validation-options-dialog.svg new file mode 100644 index 0000000..7993c23 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-UseIAM-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-UseOAuth-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-UseOAuth-dialog.svg new file mode 100644 index 0000000..84d8b48 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-UseOAuth-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + + Validation Options + + Name + + UseOAuth + Display Name + + Use OAuth 2.0 Provider + Type + + Bool + + Default Value + + True + + + False + + Not Set + Depends On + + + Use Default SDK Auth + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-field-UseOAuth-validation-options-dialog.svg b/docsource/images/AWS-ACM-v3-custom-field-UseOAuth-validation-options-dialog.svg new file mode 100644 index 0000000..7993c23 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-field-UseOAuth-validation-options-dialog.svg @@ -0,0 +1,39 @@ + + + + + + + + + Edit Custom Field + × + + + + Basic Information + Validation Options + + + Creating a certificate store + + Optional + + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-custom-fields-store-type-dialog.svg b/docsource/images/AWS-ACM-v3-custom-fields-store-type-dialog.svg new file mode 100644 index 0000000..46dbce0 --- /dev/null +++ b/docsource/images/AWS-ACM-v3-custom-fields-store-type-dialog.svg @@ -0,0 +1,157 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + + Entry Parameters + + + + + + ADD + + EDIT + + DELETE + Total: 13 + + + Display Name + Type + Default Value / Options + + + + + + + + + + + Use Default SDK Auth + Bool + false + + + + + + + Assume new Role using Default SDK ... + Bool + false + + + + + + + Use OAuth 2.0 Provider + Bool + false + + + + + + + OAuth Scope + String + + + + + + + OAuth Grant Type + String + client_credentials + + + + + + + OAuth Url + String + https://***/oauth2/default/v1/token + + + + + + + OAuth Client ID + Secret + + + + + + + OAuth Client Secret + Secret + + + + + + + Use IAM User Auth + Bool + false + + + + + + + IAM User Access Key + Secret + + + + + + + IAM User Access Secret + Secret + + + + + + + sts:ExternalId + String + + + + + + + Discovery Role Name + String + KeyfactorACMDiscoveryRole + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags-validation-options.svg b/docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags-validation-options.svg new file mode 100644 index 0000000..ddaa9ab --- /dev/null +++ b/docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags-validation-options.svg @@ -0,0 +1,68 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + Validation Options + + + Entry has a private key + + + Optional + + Required + + Ignore + + + + Job Types + + Adding an entry + + + Optional + + Required + + Hidden + Removing an entry + + + Optional + + Required + + Hidden + On Device Key Generation + + + Optional + + Required + + Hidden + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags.svg b/docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags.svg new file mode 100644 index 0000000..c4ce40f --- /dev/null +++ b/docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags.svg @@ -0,0 +1,51 @@ + + + + + + + + + Edit Entry Parameter + × + + + + Basic Information + + Validation Options + + Name + + ACM Tags + Display Name + + ACM Tags + Type + + String + + Default Value + + + Multiple Choice Options + + + Depends On + + + + + + CANCEL + + SAVE + \ No newline at end of file diff --git a/docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog.svg b/docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog.svg new file mode 100644 index 0000000..c465d8b --- /dev/null +++ b/docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog.svg @@ -0,0 +1,54 @@ + + + + + + + + + Edit Certificate Store Type + + + + Basic + Advanced + Custom Fields + Entry Parameters + + + + + + + ADD + + EDIT + + DELETE + Total: 1 + + + Display Name + Type + Default Value + + + + + + + + + + + ACM Tags + String + \ No newline at end of file diff --git a/scripts/store_types/bash/curl_create_store_types.sh b/scripts/store_types/bash/curl_create_store_types.sh index ad15eb6..42f0708 100755 --- a/scripts/store_types/bash/curl_create_store_types.sh +++ b/scripts/store_types/bash/curl_create_store_types.sh @@ -1,78 +1,20 @@ -#!/usr/bin/env bash +#!/bin/bash +# Store Type creation script using curl +# Generated by Doctool -# Creates all 1 store types via the Keyfactor Command REST API using curl. -# -# Authentication (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN -# -# Always required: -# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +set -e -if [ -z "${KEYFACTOR_HOSTNAME}" ]; then - echo "ERROR: KEYFACTOR_HOSTNAME is required" - exit 1 -fi +# Configuration - set these variables before running +KEYFACTOR_HOSTNAME="${KEYFACTOR_HOSTNAME}" +KEYFACTOR_API_PATH="${KEYFACTOR_API_PATH:-KeyfactorAPI}" +KEYFACTOR_AUTH_TOKEN="${KEYFACTOR_AUTH_TOKEN}" -BASE_URL="https://${KEYFACTOR_HOSTNAME}/keyfactorapi" - -# --------------------------------------------------------------------------- -# Resolve auth -# --------------------------------------------------------------------------- -if [ -n "${KEYFACTOR_AUTH_ACCESS_TOKEN}" ]; then - BEARER_TOKEN="${KEYFACTOR_AUTH_ACCESS_TOKEN}" -elif [ -n "${KEYFACTOR_AUTH_CLIENT_ID}" ] && [ -n "${KEYFACTOR_AUTH_CLIENT_SECRET}" ] && [ -n "${KEYFACTOR_AUTH_TOKEN_URL}" ]; then - echo "Fetching OAuth token..." - BEARER_TOKEN=$(curl -s -X POST "${KEYFACTOR_AUTH_TOKEN_URL}" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "grant_type=client_credentials" \ - --data-urlencode "client_id=${KEYFACTOR_AUTH_CLIENT_ID}" \ - --data-urlencode "client_secret=${KEYFACTOR_AUTH_CLIENT_SECRET}" | jq -r '.access_token') - if [ -z "${BEARER_TOKEN}" ] || [ "${BEARER_TOKEN}" = "null" ]; then - echo "ERROR: Failed to fetch OAuth token from ${KEYFACTOR_AUTH_TOKEN_URL}" - exit 1 - fi -elif [ -n "${KEYFACTOR_USERNAME}" ] && [ -n "${KEYFACTOR_PASSWORD}" ] && [ -n "${KEYFACTOR_DOMAIN}" ]; then - BEARER_TOKEN="" -else - echo "ERROR: Authentication required. Set one of:" - echo " KEYFACTOR_AUTH_ACCESS_TOKEN" - echo " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL" - echo " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN" - exit 1 -fi - -if [ -n "${BEARER_TOKEN}" ]; then - CURL_AUTH=("-H" "Authorization: Bearer ${BEARER_TOKEN}") -else - CURL_AUTH=("-u" "${KEYFACTOR_USERNAME}@${KEYFACTOR_DOMAIN}:${KEYFACTOR_PASSWORD}") -fi - -create_store_type() { - local name="$1" - local body="$2" - echo "Creating ${name} store type..." - response=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST "${BASE_URL}/certificatestoretypes" \ - -H "Content-Type: application/json" \ - -H "x-keyfactor-requested-with: APIClient" \ - "${CURL_AUTH[@]}" \ - -d "${body}") - if [ "$response" = "200" ] || [ "$response" = "201" ]; then - echo " OK (HTTP ${response})" - else - echo " FAILED (HTTP ${response})" - fi -} - -# --------------------------------------------------------------------------- -# AWS-ACM-v3 — This is a full AWS ARN specifying a Role. This is the Role that will be assumed in any Auth scenario performing Assume Role. This will dictate what certificates are usable by the orchestrator. A preceding [profile] name should be included if a Credential Profile is to be used in Default Sdk Auth. -# --------------------------------------------------------------------------- -create_store_type "AWS-ACM-v3" '{ +echo "Creating store type: AWS-ACM-v3" +curl -s -X POST "https://${KEYFACTOR_HOSTNAME}/${KEYFACTOR_API_PATH}/CertificateStoreTypes" \ + -H "Authorization: Bearer ${KEYFACTOR_AUTH_TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-keyfactor-requested-with: APIClient" \ + -d '{ "Name": "AWS Certificate Manager v3", "ShortName": "AWS-ACM-v3", "Capability": "AWS-ACM-v3", @@ -92,7 +34,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "", "DefaultValue": "false", "Required": true, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "A switch to enable the store to use Default SDK credentials" }, { "Name": "DefaultSdkAssumeRole", @@ -101,7 +44,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "UseDefaultSdkAuth", "DefaultValue": "false", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "A switch to enable the store to assume a new Role when using Default SDK credentials" }, { "Name": "UseOAuth", @@ -110,7 +54,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "", "DefaultValue": "false", "Required": true, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "A switch to enable the store to use an OAuth provider workflow to authenticate with AWS" }, { "Name": "OAuthScope", @@ -119,7 +64,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "UseOAuth", "DefaultValue": "", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "This is the OAuth Scope needed for Okta OAuth, defined in Okta" }, { "Name": "OAuthGrantType", @@ -128,7 +74,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "UseOAuth", "DefaultValue": "client_credentials", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "In OAuth 2.0, the term 'grant type' refers to the way an application gets an access token. In Okta this is `client_credentials`" }, { "Name": "OAuthUrl", @@ -137,7 +84,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "UseOAuth", "DefaultValue": "https://***/oauth2/default/v1/token", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "An optional parameter sts:ExternalId to pass with Assume Role calls" }, { "Name": "OAuthClientId", @@ -146,7 +94,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": true + "IsPAMEligible": true, + "Description": "The Client ID for OAuth." }, { "Name": "OAuthClientSecret", @@ -155,7 +104,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": true + "IsPAMEligible": true, + "Description": "The Client Secret for OAuth." }, { "Name": "UseIAM", @@ -164,7 +114,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "", "DefaultValue": "false", "Required": true, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "A switch to enable the store to use IAM User auth to assume a role when authenticating with AWS" }, { "Name": "IAMUserAccessKey", @@ -173,7 +124,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": true + "IsPAMEligible": true, + "Description": "The AWS Access Key for an IAM User" }, { "Name": "IAMUserAccessSecret", @@ -182,7 +134,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": true + "IsPAMEligible": true, + "Description": "The AWS Access Secret for an IAM User." }, { "Name": "ExternalId", @@ -191,7 +144,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "An optional parameter sts:ExternalId to pass with Assume Role calls" }, { "Name": "DiscoveryRoleName", @@ -200,7 +154,8 @@ create_store_type "AWS-ACM-v3" '{ "DependsOn": "", "DefaultValue": "KeyfactorACMDiscoveryRole", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "The IAM role name that exists in each target account. Used during cross-account Discovery to construct the Role ARN for cross-account access (arn:aws:iam:::role/). During a Discovery job this is supplied via the 'File name patterns to match' dialog field; this store property documents and defaults the value." } ], "EntryParameters": [ @@ -226,9 +181,6 @@ create_store_type "AWS-ACM-v3" '{ "ServerRequired": false, "PowerShell": false, "BlueprintAllowed": true, - "CustomAliasAllowed": "Optional", - "StorePathDescription": "The AWS Region the store operates in (e.g. us-east-1). Stores created by cross-account Discovery instead use a self-contained path of the form '|' (e.g. 'arn:aws:iam::123456789012:role/KeyfactorACMDiscoveryRole|us-east-1') which carries both the Role ARN to assume and the Region; for those stores the Client Machine field is informational only. Additional regions should get their own store defined." + "CustomAliasAllowed": "Optional" }' - -echo "Completed." diff --git a/scripts/store_types/bash/kfutil_create_store_types.sh b/scripts/store_types/bash/kfutil_create_store_types.sh index 9624215..001d2c8 100755 --- a/scripts/store_types/bash/kfutil_create_store_types.sh +++ b/scripts/store_types/bash/kfutil_create_store_types.sh @@ -1,28 +1,9 @@ -#!/usr/bin/env bash +#!/bin/bash +# Store Type creation script using kfutil +# Generated by Doctool -# Creates all 1 store types using kfutil. -# kfutil reads definitions from the Keyfactor integration catalog. -# -# Auth environment variables (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD -# + KEYFACTOR_DOMAIN -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +set -e -if ! command -v kfutil &> /dev/null; then - echo "kfutil could not be found. Please install kfutil" - echo "See https://github.com/Keyfactor/kfutil#quickstart" - exit 1 -fi +echo "Creating store type: AWS-ACM-v3" +kfutil store-types create AWS-ACM-v3 -if [ -z "$KEYFACTOR_HOSTNAME" ]; then - echo "KEYFACTOR_HOSTNAME not set — launching kfutil login" - kfutil login -fi - -kfutil store-types create --name "AWS-ACM-v3" - -echo "Done. All store types created." diff --git a/scripts/store_types/powershell/kfutil_create_store_types.ps1 b/scripts/store_types/powershell/kfutil_create_store_types.ps1 index cde2f4b..0c0f9af 100644 --- a/scripts/store_types/powershell/kfutil_create_store_types.ps1 +++ b/scripts/store_types/powershell/kfutil_create_store_types.ps1 @@ -1,29 +1,6 @@ -# Creates all 1 store types using kfutil. -# kfutil reads definitions from the Keyfactor integration catalog. -# -# Auth environment variables (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD -# + KEYFACTOR_DOMAIN -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +# Store Type creation script using kfutil +# Generated by Doctool -# Uncomment if kfutil is not in your PATH -# Set-Alias -Name kfutil -Value 'C:\Program Files\Keyfactor\kfutil\kfutil.exe' +Write-Host "Creating store type: AWS-ACM-v3" +kfutil store-types create AWS-ACM-v3 -if ($null -eq (Get-Command "kfutil" -ErrorAction SilentlyContinue)) { - Write-Host "kfutil could not be found. Please install kfutil" - Write-Host "See https://github.com/Keyfactor/kfutil#quickstart" - exit 1 -} - -if (-not $env:KEYFACTOR_HOSTNAME) { - Write-Host "KEYFACTOR_HOSTNAME not set — launching kfutil login" - & kfutil login -} - -& kfutil store-types create --name "AWS-ACM-v3" - -Write-Host "Done. All store types created." diff --git a/scripts/store_types/powershell/restmethod_create_store_types.ps1 b/scripts/store_types/powershell/restmethod_create_store_types.ps1 index 3519c66..9eeab29 100644 --- a/scripts/store_types/powershell/restmethod_create_store_types.ps1 +++ b/scripts/store_types/powershell/restmethod_create_store_types.ps1 @@ -1,70 +1,19 @@ -# Creates all 1 store types via the Keyfactor Command REST API -# using PowerShell Invoke-RestMethod. -# -# Authentication (first matching method is used): -# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN -# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET -# + KEYFACTOR_AUTH_TOKEN_URL -# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN -# -# Always required: -# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) -# -# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. +# Store Type creation script using Invoke-RestMethod +# Generated by Doctool -if (-not $env:KEYFACTOR_HOSTNAME) { - Write-Error "KEYFACTOR_HOSTNAME is required" - exit 1 -} - -$uri = "https://$($env:KEYFACTOR_HOSTNAME)/keyfactorapi/certificatestoretypes" -$headers = @{ - 'Content-Type' = "application/json" - 'x-keyfactor-requested-with' = "APIClient" -} +# Configuration - set these variables before running +$KeyfactorHostname = $env:KEYFACTOR_HOSTNAME +$KeyfactorApiPath = if ($env:KEYFACTOR_API_PATH) { $env:KEYFACTOR_API_PATH } else { "KeyfactorAPI" } +$KeyfactorAuthToken = $env:KEYFACTOR_AUTH_TOKEN -# --------------------------------------------------------------------------- -# Resolve auth -# --------------------------------------------------------------------------- -if ($env:KEYFACTOR_AUTH_ACCESS_TOKEN) { - $headers['Authorization'] = "Bearer $($env:KEYFACTOR_AUTH_ACCESS_TOKEN)" -} elseif ($env:KEYFACTOR_AUTH_CLIENT_ID -and $env:KEYFACTOR_AUTH_CLIENT_SECRET -and $env:KEYFACTOR_AUTH_TOKEN_URL) { - Write-Host "Fetching OAuth token..." - $tokenBody = @{ - grant_type = 'client_credentials' - client_id = $env:KEYFACTOR_AUTH_CLIENT_ID - client_secret = $env:KEYFACTOR_AUTH_CLIENT_SECRET - } - $tokenResp = Invoke-RestMethod -Method Post -Uri $env:KEYFACTOR_AUTH_TOKEN_URL -Body $tokenBody - $headers['Authorization'] = "Bearer $($tokenResp.access_token)" -} elseif ($env:KEYFACTOR_USERNAME -and $env:KEYFACTOR_PASSWORD -and $env:KEYFACTOR_DOMAIN) { - $cred = [System.Convert]::ToBase64String( - [System.Text.Encoding]::ASCII.GetBytes( - "$($env:KEYFACTOR_USERNAME)@$($env:KEYFACTOR_DOMAIN):$($env:KEYFACTOR_PASSWORD)")) - $headers['Authorization'] = "Basic $cred" -} else { - Write-Error ("Authentication required. Set one of:`n" + - " KEYFACTOR_AUTH_ACCESS_TOKEN`n" + - " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL`n" + - " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN") - exit 1 -} - -function New-StoreType { - param([string]$Name, [string]$Body) - Write-Host "Creating $Name store type..." - try { - Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $Body -ContentType "application/json" | Out-Null - Write-Host " OK" - } catch { - Write-Warning " FAILED: $($_.Exception.Message)" - } +$Headers = @{ + "Authorization" = "Bearer $KeyfactorAuthToken" + "Content-Type" = "application/json" + "x-keyfactor-requested-with" = "APIClient" } -# --------------------------------------------------------------------------- -# AWS-ACM-v3 — This is a full AWS ARN specifying a Role. This is the Role that will be assumed in any Auth scenario performing Assume Role. This will dictate what certificates are usable by the orchestrator. A preceding [profile] name should be included if a Credential Profile is to be used in Default Sdk Auth. -# --------------------------------------------------------------------------- -New-StoreType "AWS-ACM-v3" @' +Write-Host "Creating store type: AWS-ACM-v3" +$Body = @' { "Name": "AWS Certificate Manager v3", "ShortName": "AWS-ACM-v3", @@ -85,7 +34,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "", "DefaultValue": "false", "Required": true, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "A switch to enable the store to use Default SDK credentials" }, { "Name": "DefaultSdkAssumeRole", @@ -94,7 +44,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "UseDefaultSdkAuth", "DefaultValue": "false", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "A switch to enable the store to assume a new Role when using Default SDK credentials" }, { "Name": "UseOAuth", @@ -103,7 +54,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "", "DefaultValue": "false", "Required": true, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "A switch to enable the store to use an OAuth provider workflow to authenticate with AWS" }, { "Name": "OAuthScope", @@ -112,7 +64,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "UseOAuth", "DefaultValue": "", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "This is the OAuth Scope needed for Okta OAuth, defined in Okta" }, { "Name": "OAuthGrantType", @@ -121,7 +74,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "UseOAuth", "DefaultValue": "client_credentials", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "In OAuth 2.0, the term 'grant type' refers to the way an application gets an access token. In Okta this is `client_credentials`" }, { "Name": "OAuthUrl", @@ -130,7 +84,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "UseOAuth", "DefaultValue": "https://***/oauth2/default/v1/token", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "An optional parameter sts:ExternalId to pass with Assume Role calls" }, { "Name": "OAuthClientId", @@ -139,7 +94,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": true + "IsPAMEligible": true, + "Description": "The Client ID for OAuth." }, { "Name": "OAuthClientSecret", @@ -148,7 +104,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": true + "IsPAMEligible": true, + "Description": "The Client Secret for OAuth." }, { "Name": "UseIAM", @@ -157,7 +114,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "", "DefaultValue": "false", "Required": true, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "A switch to enable the store to use IAM User auth to assume a role when authenticating with AWS" }, { "Name": "IAMUserAccessKey", @@ -166,7 +124,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": true + "IsPAMEligible": true, + "Description": "The AWS Access Key for an IAM User" }, { "Name": "IAMUserAccessSecret", @@ -175,7 +134,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": true + "IsPAMEligible": true, + "Description": "The AWS Access Secret for an IAM User." }, { "Name": "ExternalId", @@ -184,7 +144,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "", "DefaultValue": "", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "An optional parameter sts:ExternalId to pass with Assume Role calls" }, { "Name": "DiscoveryRoleName", @@ -193,7 +154,8 @@ New-StoreType "AWS-ACM-v3" @' "DependsOn": "", "DefaultValue": "KeyfactorACMDiscoveryRole", "Required": false, - "IsPAMEligible": false + "IsPAMEligible": false, + "Description": "The IAM role name that exists in each target account. Used during cross-account Discovery to construct the Role ARN for cross-account access (arn:aws:iam:::role/). During a Discovery job this is supplied via the 'File name patterns to match' dialog field; this store property documents and defaults the value." } ], "EntryParameters": [ @@ -219,10 +181,9 @@ New-StoreType "AWS-ACM-v3" @' "ServerRequired": false, "PowerShell": false, "BlueprintAllowed": true, - "CustomAliasAllowed": "Optional", - "StorePathDescription": "The AWS Region the store operates in (e.g. us-east-1). Stores created by cross-account Discovery instead use a self-contained path of the form '|' (e.g. 'arn:aws:iam::123456789012:role/KeyfactorACMDiscoveryRole|us-east-1') which carries both the Role ARN to assume and the Region; for those stores the Client Machine field is informational only. Additional regions should get their own store defined." + "CustomAliasAllowed": "Optional" } '@ +Invoke-RestMethod -Uri "https://$KeyfactorHostname/$KeyfactorApiPath/CertificateStoreTypes" -Method POST -Headers $Headers -Body $Body -Write-Host "Completed." From 88de8b1bfb117b3e5d8220aa43f77b4dc869bc5a Mon Sep 17 00:00:00 2001 From: Mark Kachkaev Date: Tue, 4 Aug 2026 15:34:37 -0400 Subject: [PATCH 3/6] Added correct project file. --- .../aws-acm-orchestrator.csproj | 61 +++++++++---------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/aws-acm-orchestrator/aws-acm-orchestrator.csproj b/aws-acm-orchestrator/aws-acm-orchestrator.csproj index 8869e49..2a79c9b 100644 --- a/aws-acm-orchestrator/aws-acm-orchestrator.csproj +++ b/aws-acm-orchestrator/aws-acm-orchestrator.csproj @@ -1,38 +1,33 @@  - - true - net6.0;net8.0 - true - disable - Keyfactor.Extensions.Orchestrator.Aws.Acm - Keyfactor.Extensions.Orchestrator.Aws.Acm - + + true + net8.0;net10.0 + true + disable + Keyfactor.Extensions.Orchestrator.Aws.Acm + Keyfactor.Extensions.Orchestrator.Aws.Acm + - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + Always + + - - Always - - - - + \ No newline at end of file From c26dab44442b3cc6c65d5c3ae10222131740b148 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Aug 2026 19:35:27 +0000 Subject: [PATCH 4/6] docs: auto-generate README and documentation [skip ci] --- README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0115507..120a170 100644 --- a/README.md +++ b/README.md @@ -290,78 +290,91 @@ the Keyfactor Command Portal A switch to enable the store to use Default SDK credentials ![AWS-ACM-v3 Custom Field - UseDefaultSdkAuth](docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-dialog.svg) + ![AWS-ACM-v3 Custom Field - UseDefaultSdkAuth](docsource/images/AWS-ACM-v3-custom-field-UseDefaultSdkAuth-validation-options-dialog.svg) ###### Assume new Role using Default SDK Auth A switch to enable the store to assume a new Role when using Default SDK credentials ![AWS-ACM-v3 Custom Field - DefaultSdkAssumeRole](docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-dialog.svg) + ![AWS-ACM-v3 Custom Field - DefaultSdkAssumeRole](docsource/images/AWS-ACM-v3-custom-field-DefaultSdkAssumeRole-validation-options-dialog.svg) ###### Use OAuth 2.0 Provider A switch to enable the store to use an OAuth provider workflow to authenticate with AWS ![AWS-ACM-v3 Custom Field - UseOAuth](docsource/images/AWS-ACM-v3-custom-field-UseOAuth-dialog.svg) + ![AWS-ACM-v3 Custom Field - UseOAuth](docsource/images/AWS-ACM-v3-custom-field-UseOAuth-validation-options-dialog.svg) ###### OAuth Scope This is the OAuth Scope needed for Okta OAuth, defined in Okta ![AWS-ACM-v3 Custom Field - OAuthScope](docsource/images/AWS-ACM-v3-custom-field-OAuthScope-dialog.svg) + ![AWS-ACM-v3 Custom Field - OAuthScope](docsource/images/AWS-ACM-v3-custom-field-OAuthScope-validation-options-dialog.svg) ###### OAuth Grant Type In OAuth 2.0, the term 'grant type' refers to the way an application gets an access token. In Okta this is `client_credentials` ![AWS-ACM-v3 Custom Field - OAuthGrantType](docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-dialog.svg) + ![AWS-ACM-v3 Custom Field - OAuthGrantType](docsource/images/AWS-ACM-v3-custom-field-OAuthGrantType-validation-options-dialog.svg) ###### OAuth Url An optional parameter sts:ExternalId to pass with Assume Role calls ![AWS-ACM-v3 Custom Field - OAuthUrl](docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-dialog.svg) + ![AWS-ACM-v3 Custom Field - OAuthUrl](docsource/images/AWS-ACM-v3-custom-field-OAuthUrl-validation-options-dialog.svg) ###### OAuth Client ID The Client ID for OAuth. ![AWS-ACM-v3 Custom Field - OAuthClientId](docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-dialog.svg) + ![AWS-ACM-v3 Custom Field - OAuthClientId](docsource/images/AWS-ACM-v3-custom-field-OAuthClientId-validation-options-dialog.svg) ###### OAuth Client Secret The Client Secret for OAuth. ![AWS-ACM-v3 Custom Field - OAuthClientSecret](docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-dialog.svg) + ![AWS-ACM-v3 Custom Field - OAuthClientSecret](docsource/images/AWS-ACM-v3-custom-field-OAuthClientSecret-validation-options-dialog.svg) ###### Use IAM User Auth A switch to enable the store to use IAM User auth to assume a role when authenticating with AWS ![AWS-ACM-v3 Custom Field - UseIAM](docsource/images/AWS-ACM-v3-custom-field-UseIAM-dialog.svg) + ![AWS-ACM-v3 Custom Field - UseIAM](docsource/images/AWS-ACM-v3-custom-field-UseIAM-validation-options-dialog.svg) ###### IAM User Access Key The AWS Access Key for an IAM User ![AWS-ACM-v3 Custom Field - IAMUserAccessKey](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-dialog.svg) + ![AWS-ACM-v3 Custom Field - IAMUserAccessKey](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessKey-validation-options-dialog.svg) ###### IAM User Access Secret The AWS Access Secret for an IAM User. ![AWS-ACM-v3 Custom Field - IAMUserAccessSecret](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-dialog.svg) + ![AWS-ACM-v3 Custom Field - IAMUserAccessSecret](docsource/images/AWS-ACM-v3-custom-field-IAMUserAccessSecret-validation-options-dialog.svg) ###### sts:ExternalId An optional parameter sts:ExternalId to pass with Assume Role calls ![AWS-ACM-v3 Custom Field - ExternalId](docsource/images/AWS-ACM-v3-custom-field-ExternalId-dialog.svg) + ![AWS-ACM-v3 Custom Field - ExternalId](docsource/images/AWS-ACM-v3-custom-field-ExternalId-validation-options-dialog.svg) ###### Discovery Role Name The IAM role name that exists in each target account. Used during cross-account Discovery to construct the Role ARN for cross-account access (arn:aws:iam:::role/). During a Discovery job this is supplied via the 'File name patterns to match' dialog field; this store property documents and defaults the value. ![AWS-ACM-v3 Custom Field - DiscoveryRoleName](docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-dialog.svg) + ![AWS-ACM-v3 Custom Field - DiscoveryRoleName](docsource/images/AWS-ACM-v3-custom-field-DiscoveryRoleName-validation-options-dialog.svg) ##### Entry Parameters Tab @@ -377,6 +390,7 @@ the Keyfactor Command Portal The optional ACM tags that should be assigned to the certificate. Multiple name/value pairs may be entered in the format of `Name1=Value1,Name2=Value2,...,NameN=ValueN` ![AWS-ACM-v3 Entry Parameter - ACM Tags](docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags.svg) + ![AWS-ACM-v3 Entry Parameter - ACM Tags](docsource/images/AWS-ACM-v3-entry-parameters-store-type-dialog-ACM Tags-validation-options.svg) @@ -389,15 +403,13 @@ the Keyfactor Command Portal | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `aws-orchestrator` .NET version to download | | --------- | ----------- | ----------- | ----------- | - | Older than `11.0.0` | | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` | | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | | `11.6` _and_ newer | `net8.0` | | `net8.0` | + | `25.5` _and_ newer | `net10.0` | | `net10.0` | Unzip the archive containing extension assemblies to a known location. - > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net8.0`. + > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net10.0`. 2. **Locate the Universal Orchestrator extensions directory.** From 585cf0fe813ab35e76ea580d0a350d91816dc9cf Mon Sep 17 00:00:00 2001 From: Mark Kachkaev <37276742+mkachk@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:12:38 -0400 Subject: [PATCH 5/6] Small change to retrigger build Updated the CHANGELOG to reflect new features and bug fixes. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cafdf16..a14f87c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ * Discovery enumerates AWS Organizations accounts via `organizations:ListAccounts`, assumes a per-account IAM role (configurable via the new `DiscoveryRoleName` property; default `KeyfactorACMDiscoveryRole`), and scans each account's enabled regions for ACM certificates * Discovered certificate stores use a self-contained `Store Path` of the form `|` that carries both the Role ARN to assume and the AWS Region in a single field; legacy region-only `Store Path` values (with the Role ARN in `Client Machine`) continue to work * Added the `DiscoveryRoleName` store type property -* Discovery, Inventory, and Management jobs now return descriptive success, warning, and failure messages to Keyfactor Command that identify the exact store (and, for Discovery, the accounts and regions scanned) instead of directing the user to the logs +* Discovery, Inventory, and Management jobs now return descriptive success, warning, and failure messages to Keyfactor Command that identify the exact store (and, for Discovery, the accounts and regions scanned) instead of directing the user to the logs . 3.0.2 * Bug Fix - On Management jobs, do not send ACM tags if the certificate is being renewed/replaced From 9e8ef821a5e3f97c4ae50d0bee5cbfc0631dd8f6 Mon Sep 17 00:00:00 2001 From: Mark Kachkaev Date: Tue, 4 Aug 2026 17:48:43 -0400 Subject: [PATCH 6/6] Fix leaf cert regression --- aws-acm-orchestrator/Jobs/Management.cs | 36 ++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/aws-acm-orchestrator/Jobs/Management.cs b/aws-acm-orchestrator/Jobs/Management.cs index d961bea..4053771 100644 --- a/aws-acm-orchestrator/Jobs/Management.cs +++ b/aws-acm-orchestrator/Jobs/Management.cs @@ -147,10 +147,10 @@ internal JobResult PerformAddition(AwsExtensionCredential awsCredentials, Manage if (!string.IsNullOrWhiteSpace(config.JobCertificate.PrivateKeyPassword)) // This is a PFX Entry { Logger.LogTrace($"Found Private Key password."); - if (!string.IsNullOrWhiteSpace(config.JobCertificate.Alias)) + if (IsAcmCertificateArn(config.JobCertificate.Alias)) { - // Alias is specified, this is a replace / renewal - Logger.LogDebug($"Alias specified, validating existing cert can be renewed / replaced: {config.JobCertificate.Alias}"); + // Alias is an ACM certificate ARN, so this is a replace / renewal of an existing cert + Logger.LogDebug($"ACM ARN supplied as alias, validating existing cert can be renewed / replaced: {config.JobCertificate.Alias}"); // ARN Provided, Verify It is Not A PCA/Amazon Issued Cert DescribeCertificateResponse DescribeCertificateResponse = AsyncHelpers.RunSync(() => AcmClient.DescribeCertificateAsync(config.JobCertificate.Alias)); Logger.LogTrace($"DescribeCertificateResponse JSON: {JsonConvert.SerializeObject(DescribeCertificateResponse)}"); @@ -227,7 +227,7 @@ internal JobResult PerformAddition(AwsExtensionCredential awsCredentials, Manage CertificateChain = chainStream }; - icr.CertificateArn = config.JobCertificate.Alias?.Length >= 20 ? config.JobCertificate.Alias.Trim() : null; //If an arn is provided, use it, this will perform a renewal/replace + icr.CertificateArn = IsAcmCertificateArn(config.JobCertificate.Alias) ? config.JobCertificate.Alias.Trim() : null; //If an ACM certificate ARN is provided, reimport in place (renewal/replace); otherwise import as a new certificate Logger.LogTrace($"Certificate arn {icr.CertificateArn}"); if (icr.CertificateArn == null && acmTags != null && acmTags.Count > 0) @@ -377,6 +377,18 @@ private static MemoryStream GetChain(Pkcs12Store store, string alias) X509CertificateEntry[] chain = store.GetCertificateChain(alias); + // BouncyCastle returns the chain with the leaf/end-entity certificate as element [0], + // followed by any intermediates (and root). The leaf is already sent separately as the + // Certificate body of the ImportCertificateRequest, so it must NOT be repeated here; + // ACM's CertificateChain is expected to contain only the intermediate (and root) certs. + // Including the leaf caused it to appear twice within the published certificate's chain. + if (chain == null || chain.Length <= 1) + { + // Only the leaf is present (no intermediates) - omit the chain entirely rather than + // sending an empty value, which ACM may reject as an unparseable certificate chain. + return null; + } + foreach (X509CertificateEntry chainEntry in chain.Skip(1)) { ccs += certStart + _pemify(Convert.ToBase64String(chainEntry.Certificate.GetEncoded())) + certEnd + "\n"; @@ -393,6 +405,22 @@ private static MemoryStream CertStringToStream(string certString) return new MemoryStream(certBytes); } + /// + /// Determines whether the supplied alias is an AWS Certificate Manager certificate ARN + /// (e.g. arn:aws:acm:<region>:<account>:certificate/<id>). When it is, the certificate + /// already exists in ACM and an Add job should reimport in place (renewal/replace); + /// otherwise the certificate is imported as a new one and ACM assigns a fresh ARN. + /// Replaces an earlier alias-length heuristic that could misclassify a long friendly alias. + /// + internal static bool IsAcmCertificateArn(string alias) + { + if (string.IsNullOrWhiteSpace(alias)) return false; + + string trimmed = alias.Trim(); + return trimmed.StartsWith("arn:aws:acm:", StringComparison.OrdinalIgnoreCase) + && trimmed.Contains(":certificate/"); + } + private List ParseACMTags(Dictionary jobProperties) { List acmTags = new List();