diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml new file mode 100644 index 0000000..a3dab32 --- /dev/null +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -0,0 +1,26 @@ +name: Starter Workflow +on: [workflow_dispatch, push, pull_request] + +jobs: + call-create-github-release-workflow: + uses: Keyfactor/actions/.github/workflows/github-release.yml@main + + call-dotnet-build-and-release-workflow: + needs: [call-create-github-release-workflow] + uses: Keyfactor/actions/.github/workflows/dotnet-build-and-release.yml@main + with: + release_version: ${{ needs.call-create-github-release-workflow.outputs.release_version }} + release_url: ${{ needs.call-create-github-release-workflow.outputs.release_url }} + release_dir: EXAMPLE_SOLUTION/bin/Release/BUILD_TARGET # TODO: set build output directory to upload as a release, relative to checkout workspace + secrets: + token: ${{ secrets.PRIVATE_PACKAGE_ACCESS }} + + call-generate-readme-workflow: + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + uses: Keyfactor/actions/.github/workflows/generate-readme.yml@main + + call-update-catalog-workflow: + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + uses: Keyfactor/actions/.github/workflows/update-catalog.yml@main + secrets: + token: ${{ secrets.SDK_SYNC_PAT }} diff --git a/Media/Images/AddPubCert.gif b/Media/Images/AddPubCert.gif new file mode 100644 index 0000000..e6e3db1 Binary files /dev/null and b/Media/Images/AddPubCert.gif differ diff --git a/Media/Images/CertStore1.gif b/Media/Images/CertStore1.gif new file mode 100644 index 0000000..01f7b69 Binary files /dev/null and b/Media/Images/CertStore1.gif differ diff --git a/Media/Images/CertStore2.gif b/Media/Images/CertStore2.gif new file mode 100644 index 0000000..e52ddbe Binary files /dev/null and b/Media/Images/CertStore2.gif differ diff --git a/Media/Images/CertStoreType-Advanced.gif b/Media/Images/CertStoreType-Advanced.gif new file mode 100644 index 0000000..077fa44 Binary files /dev/null and b/Media/Images/CertStoreType-Advanced.gif differ diff --git a/Media/Images/CertStoreType-Basic.gif b/Media/Images/CertStoreType-Basic.gif new file mode 100644 index 0000000..3a21c8a Binary files /dev/null and b/Media/Images/CertStoreType-Basic.gif differ diff --git a/Media/Images/CertStoreType-CustomFields.gif b/Media/Images/CertStoreType-CustomFields.gif new file mode 100644 index 0000000..9e0cd8f Binary files /dev/null and b/Media/Images/CertStoreType-CustomFields.gif differ diff --git a/Media/Images/CertStoreType-EntryParameters.gif b/Media/Images/CertStoreType-EntryParameters.gif new file mode 100644 index 0000000..277e052 Binary files /dev/null and b/Media/Images/CertStoreType-EntryParameters.gif differ diff --git a/Media/Images/CertificateInventory.gif b/Media/Images/CertificateInventory.gif new file mode 100644 index 0000000..6544ffc Binary files /dev/null and b/Media/Images/CertificateInventory.gif differ diff --git a/Media/Images/NewCertNewAlias.gif b/Media/Images/NewCertNewAlias.gif new file mode 100644 index 0000000..201db47 Binary files /dev/null and b/Media/Images/NewCertNewAlias.gif differ diff --git a/Media/Images/PubCertReplace.gif b/Media/Images/PubCertReplace.gif new file mode 100644 index 0000000..a10a42d Binary files /dev/null and b/Media/Images/PubCertReplace.gif differ diff --git a/Media/Images/RemoveCertAndKey.gif b/Media/Images/RemoveCertAndKey.gif new file mode 100644 index 0000000..b8203ba Binary files /dev/null and b/Media/Images/RemoveCertAndKey.gif differ diff --git a/Media/Images/RemovePubCert.gif b/Media/Images/RemovePubCert.gif new file mode 100644 index 0000000..e50bc45 Binary files /dev/null and b/Media/Images/RemovePubCert.gif differ diff --git a/Media/Images/ReplaceCertSameAlias.gif b/Media/Images/ReplaceCertSameAlias.gif new file mode 100644 index 0000000..9253a82 Binary files /dev/null and b/Media/Images/ReplaceCertSameAlias.gif differ diff --git a/README.md b/README.md index 0776d5f..eb6d4ca 100644 --- a/README.md +++ b/README.md @@ -1 +1,158 @@ -# a10vthunder-orchestrator \ No newline at end of file +# a10vThunder + +A10 vThunder AnyAgent allows an organization to inventory and deploy certificates in any domain that the appliance services. The AnyAgent deploys the appropriate files (.cer, .pem) within the defined directories and also performs and Inventory on the Items. + +#### Integration status: + +## About the Keyfactor Universal Orchestrator Capability + +This repository contains a Universal Orchestrator Capability which is a plugin to the Keyfactor Universal Orchestrator. Within the Keyfactor Platform, Orchestrators are used to manage “certificate stores” — collections of certificates and roots of trust that are found within and used by various applications. + +The Universal Orchestrator is part of the Keyfactor software distribution and is available via the Keyfactor customer portal. For general instructions on installing Capabilities, see the “Keyfactor Command Orchestrator Installation and Configuration Guide” section of the Keyfactor documentation. For configuration details of this specific Capability, see below in this readme. + +The Universal Orchestrator is the successor to the Windows Orchestrator. This Capability plugin only works with the Universal Orchestrator and does not work with the Windows Orchestrator. + +--- + + + + +--- + +**A10 Networks vThunder Orchestrator** + +**Overview** + +A10 vThunder AnyAgent allows an organization to inventory and deploy certificates in any domain that the appliance services. The AnyAgent deploys the appropriate files (.cer, .pem) within the defined directories and also performs and Inventory on the Items. + +This agent implements three job types – Inventory, Management Add, and Management Remove. Below are the steps necessary to configure this AnyAgent. It supports adding certificates with or without private keys. + + +**A10 vThunder Configuration** + +1. Read up on [A10 Networks ADC](https://a10networks.optrics.com/downloads/datasheets/Thunder-Application-Delivery-Controller-ADC.pdf) and how it works. +2. A user account is needed with the appropriate permissions on vThunder to manage certificates. + +**1. Create the New Certificate Store Type for the A10 vThunder Orchestrator** + +In Keyfactor Command create a new Certificate Store Type similar to the one below: + +#### STORE TYPE CONFIGURATION +SETTING TAB | CONFIG ELEMENT | DESCRIPTION +------|-----------|------------------ +Basic |Name |Descriptive name for the Store Type. A10 vThunder can be used. +Basic |Short Name |The short name that identifies the registered functionality of the orchestrator. Must be vThunderU +Basic |Custom Capability|Un checked +Basic |Job Types |Inventory, Add, and Remove are the supported job types. +Basic |Needs Server |Must be checked +Basic |Blueprint Allowed |checked +Basic |Requires Store Password |Determines if a store password is required when configuring an individual store. This must be unchecked. +Basic |Supports Entry Password |Determined if an individual entry within a store can have a password. This must be unchecked. +Advanced |Store Path Type| Determines how the user will enter the store path when setting up the cert store. Freeform +Advanced |Supports Custom Alias |Determines if an individual entry within a store can have a custom Alias. This must be Required +Advanced |Private Key Handling |Determines how the orchestrator deals with private keys. Optional +Advanced |PFX Password Style |Determines password style for the PFX Password. Default +Custom Fields|protocol|Name:protocol Display Name:Protocol Type:Multiple Choice (http,https) Default Value:https Required:True +Custom Fields|allowInvalidCert|Name:allowInvalidCert Display Name:Allow Invalid Cert Type:Bool Default Value:false Required:True +Entry Parameters|N/A| There are no Entry Parameters + +**Basic Settings:** + +![](Media/Images/CertStoreType-Basic.gif) + +**Advanced Settings:** + +![](Media/Images/CertStoreType-Advanced.gif) + +**Custom Fields:** + +![](Media/Images/CertStoreType-CustomFields.gif) + +**Entry Params:** + +![](Media/Images/CertStoreType-EntryParameters.gif) + +**2. Register the A10 vThunder Orchestrator with Keyfactor** +See Keyfactor InstallingKeyfactorOrchestrators.pdf Documentation. Get from your Keyfactor contact/representative. + +**3. Create a A10 vThunder Certificate Store within Keyfactor Command** +In Keyfactor Command create a new Certificate Store similar to the one below + +![](Media/Images/CertStore1.gif) +![](Media/Images/CertStore2.gif) + +#### STORE CONFIGURATION +CONFIG ELEMENT |DESCRIPTION +----------------|--------------- +Category |The type of certificate store to be configured. Select category based on the display name configured above "VThunder Universal". +Container |This is a logical grouping of like stores. This configuration is optional and does not impact the functionality of the store. +Client Machine |The url to the vThunder api. This file should the url and port of the vThunder api sample vThunder.test.com:1113. +Store Path |This will be "cert". This is not used but just hard code it as "cert". +Allow Invalid Cert|Only used for testing should be false in production. +Protocol| http is only used for testing should be https in production +Orchestrator |This is the orchestrator server registered with the appropriate capabilities to manage this certificate store type. +Inventory Schedule |The interval that the system will use to report on what certificates are currently in the store. +Use SSL |This should be checked. +User |This is the user name for the vThunder api to access the certficate management functionality. +Password |This is the password for the vThunder api to access the certficate management functionality. + +*** + +#### Usage + +**Adding New Certificate New Alias** + +![](Media/Images/NewCertNewAlias.gif) + +*** + +**Replace Cert With Same Alias** + +![](Media/Images/ReplaceCertSameAlias.gif) + +*** + +**Add Cert No Private Key** + +![](Media/Images/AddPubCert.gif) + +*** + +**Replace Cert No Private Key** + +![](Media/Images/PubCertReplace.gif) + +*** + +**Remove Cert No Private Key** + +![](Media/Images/RemovePubCert.gif) + +*** + +**Remove Cert and Private Key** + +![](Media/Images/RemoveCertAndKey.gif) + +*** + +**Certificate Inventory** + +![](Media/Images/CertificateInventory.gif) + +#### TEST CASES +Case Number|Case Name|Case Description|Overwrite Flag|Alias Name|Expected Results|Passed +------------|---------|----------------|--------------|----------|----------------|-------------- +1|Fresh Add With Alias|Will create new certificate and private key on the vThunder appliance|true|KeyAndCertBTest|The new KeyAndCertBTest certificate and private key will be created in the ADC/SSL Cerificates area on vThunder.|True +1a|Replace Alias with no overwrite flag|Should warn user that a cert cannot be replaced with the same name without overwrite flag|false|KeyAndCertBTest|Error Saying Overwrite Flag Needs To Be Used|True +1b|Replace Alias with overwrite flag|Will create new certificate and private key on the vThunder appliance|true|KeyAndCertBTest|Cert will be replaced because overwrite flag was used|True +2|Add Cert Without Private Key|This will create a cert with no private key on vThunder|false|NewCertNoPk|Only Cert will be added to vThunder with no private key|True +2a|Replace Cert Without Private Key|This will Replace a cert with no private key on vThunder|true|NewCertNoPk|Only Cert will be replaced on vThunder with no private key|True +2b|Replace Cert Without Private Key no overwrite flag|Should warn user that a cert cannot be replaced with the same name without overwrite flag|false|NewCertNoPk|Error Saying Overwrite Flag Needs To Be Used|True +3|Remove Certificate and Private Key|Certificate and Private Key Will Be Removed from A10|N/A|KeyAndCertBTest|Cert and Key will be removed from vThunder and Keyfactor Store|True +3a|Remove Certificate without Private Key|Certificate Will Be Removed from A10|N/A|KeyAndCertBTest|Cert will be removed from vThunder and Keyfactor Store|True +4|Inventory Certificates with Private Key|Inventory of Certificates with private keys will be pulled from vThunder up to 125 tested|N/A|N/A|125 Certs will be inventoried, more should be supported but there is no paging in the API so limits apply|True +4a|Inventory Certificates without Private Key|Inventory of Certificates without private keys will be pulled from vThunder up to 125 tested|N/A|N/A|125 Certs will be inventoried, more should be supported but there is no paging in the API so limits apply|True + + + diff --git a/README.md.tpl b/README.md.tpl new file mode 100644 index 0000000..90db2b4 --- /dev/null +++ b/README.md.tpl @@ -0,0 +1,96 @@ +# {{ name }} +## {{ integration_type | capitalize }} + +{{ description }} + +## {{ status | capitalize }} Ready + + +*** +## **A10 vThunder Configuration** + +**Overview** + +The A10 vThunder Agent allows a user to inventory certificates and manage (add/remove/replace) certificates from the A10 vThunder platform. + +**1) Create the new Certificate store Type for the New A10 vThunder AnyAgent** + +In Keyfactor Command create a new Certificate Store Type similar to the one below: + +![image.png](/Media/Images/CertStores.gif) + + +- **Name** – Required. The display name of the new Certificate Store Type +- **Short Name** – Required. MUST be "vThunder" + +- **Needs Server, Blueprint Allowed** – checked as shown +- **Requires Store Password, Supports Entry Password** – unchecked as shown +- **Supports Custom Alias** – Forbidden. Not used. +- **Use PowerShell** – Unchecked +- **Store PathType** – Freeform (user will enter the the location of the store). +- **Private Keys** – Optional +- **PFX Password Style** – Default +- **Job Types** – Inventory, Add and Remove are the 3 job types implemented by this AnyAgent + + +**2) Register the A10 vThunder AnyAgent with Keyfactor** + +Open the Keyfactor Windows Agent Configuration Wizard and perform the tasks as illustrated below: + +![image.png](/Media/Images/ConfigWizard1.gif) + +- Click **** + +![image.png](/Media/Images/ConfigWizard2.gif) + +If you have configured the agent service previously, you should be able to skip to just click ****. Otherwise, enter the service account Username and Password you wish to run the Keyfactor Windows Agent Service under, click **** and click ****. + +![image.png](/Media/Images/ConfigWizard3.gif) + +If you have configured the agent service previously, you should be able to skip to just re-enter the password to the service account the agent service will run under, click **** and then ****. + +![image.png](/Media/Images/ConfigWizard4.gif) + +Select the agent you are adding capabilities for (in this case, vThunder, and also select the specific capabilities (Inventory and Management in this example). Click ****. + +![image.png](/Media/Images/ConfigWizard5.gif) + +For each AnyAgent implementation, check Load assemblies containing extension modules from other location , browse to the location of the compiled AnyAgent dlls, and click ****. Once all AnyAgents have been validated, click ****. + +![image.png](/Media/Images/ConfigWizard6.gif) + +If the Keyfactor Agent Configuration Wizard configured everything correctly, you should see the dialog above. + +**3) Create a Cert Store within the Keyfactor Portal** + +Navigate to Certificate Locations => Certificate Stores within Keyfactor Command to add an A10 vThunder certificate store. Below are the values that should be entered. + +![image.png](/Media/Images/CertStores.gif) + +- **Category** – Required. The vThunder category name must be selected +- **Container** – Optional. Select a container if utilized. +- **Client Machine** – Required. The server name or IP Address of the A10 vThunder API plus port. [Azure Test Machine](https://portal.azure.com/#@csspkioutlook.onmicrosoft.com/resource/subscriptions/b3114ff1-bb92-45b6-9bd6-e4a1eed8c91e/resourceGroups/kVThunderA10/providers/Microsoft.Compute/virtualMachines/kVThunderA10/overview) port is :1113 for ssl. +- **Store Path** – Required. This will be one of the following based on what you are looking to add to the store. +1. **[DomainName]\cert** where [DomainName] is the name of the domain in A10 vThunder you are looking to manage and inventory. + +2. **cert** - This will use the default domain in A10 vThunder to manage and inventory **domain** certs + +3. **[DomainName]\pubcert** - This will give you the ability to Inventory the Pub Cert Folder on the specified domain where [DomainName] is the name of the domain in A10 vThunder you are looking to inventory. + +4. **pubcert** - This will use the default domain in A10 vThunder to manage and inventory **public certs** certs + +### App Config Settings +Keyfactor.AnyAgent.vThunder.dll.config (Deployed with all AnyAgent Binaries) +``` + + + + + + +``` + +There are 2 App Config Settings +1. Protocol should always be **https** in **Production** but you may need to switch to **http** for **Testing** only + +2. AllowInvalidCerts should be set to **false** in **Production**. It is set to **true** in **Dev/Test** since the VM we are testing with a vThunder VM that does not have a valid SSL Certificate on the Azure Platform. diff --git a/Setup/InstallA10AzureVM.ps1 b/Setup/InstallA10AzureVM.ps1 new file mode 100644 index 0000000..b3cdce5 --- /dev/null +++ b/Setup/InstallA10AzureVM.ps1 @@ -0,0 +1,66 @@ +$location = Read-Host 'Enter the location' +$resourceGroup = Read-Host 'Enter resource group name' +$storageaccount = Read-Host 'Enter storage account name' +$vmName = Read-Host 'VM Name' +$vmSize = Read-Host 'Enter VM size' + +#Create new resource group for deployment +New-AzureRmResourceGroup -Name $resourceGroup -Location $location + +#Create storage account +New-AzureRmStorageAccount -ResourceGroupName $resourceGroup -AccountName $storageaccount -Location $location -SkuName Standard_RAGRS -Kind StorageV2 -AssignIdentity + +# Create a subnet configuration +$mgmtsubnet = New-AzureRmVirtualNetworkSubnetConfig -Name "mgmtSubnet" -AddressPrefix "192.168.1.0/24" +$data1subnet = New-AzureRmVirtualNetworkSubnetConfig -Name "data1subnet" -AddressPrefix "192.168.2.0/24" +$data2subnet = New-AzureRmVirtualNetworkSubnetConfig -Name "data2subnet" -AddressPrefix "192.168.3.0/24" + +# Create a virtual network +$vnet = New-AzureRmVirtualNetwork -ResourceGroupName $resourceGroup -Location $location -Name "TestVnet" -AddressPrefix 192.168.0.0/16 -Subnet $mgmtsubnet,$data1subnet,$data2subnet + +# Create a public IP address and specify a DNS name +$mgmtpip = New-AzureRmPublicIpAddress -ResourceGroupName $resourceGroup -Location $location -AllocationMethod Dynamic -IdleTimeoutInMinutes 4 -Name "myip$(Get-Random)" +$data1pip = New-AzureRmPublicIpAddress -ResourceGroupName $resourceGroup -Location $location -AllocationMethod Dynamic -IdleTimeoutInMinutes 4 -Name "myip$(Get-Random)" +$data2pip = New-AzureRmPublicIpAddress -ResourceGroupName $resourceGroup -Location $location -AllocationMethod Dynamic -IdleTimeoutInMinutes 4 -Name "myip$(Get-Random)" + +# Create an inbound network security group rule for port 22 +$nsgRuleSSH = New-AzureRmNetworkSecurityRuleConfig -Name "myNetworkSecurityGroupRuleSSH" -Protocol "Tcp" -Direction "Inbound" -Priority 1000 -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 22 -Access "Allow" +# Create an inbound network security group rule for port 80 +$nsgRuleWeb = New-AzureRmNetworkSecurityRuleConfig -Name "myNetworkSecurityGroupRuleWWW" -Protocol "Tcp" -Direction "Inbound" -Priority 1001 -SourceAddressPrefix * -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 80 -Access "Allow" + +# Create a network security group +$nsg = New-AzureRmNetworkSecurityGroup -ResourceGroupName $resourceGroup -Location $location -Name "myNetworkSecurityGroup" -SecurityRules $nsgRuleSSH,$nsgRuleWeb + +# Create a virtual network card and associate with public IP address and NSG +$mgmtsubnet = $vnet.Subnets | ?{ $_.Name -eq 'mgmtsubnet' } +$mgmtnic = New-AzureRmNetworkInterface -ResourceGroupName $resourceGroup -Name "mgmtnic" -Location $location -SubnetId $mgmtsubnet.Id -PublicIpAddressId $mgmtpip.Id -NetworkSecurityGroupId $nsg.Id + +$data1subnet = $vnet.Subnets | ?{ $_.Name -eq 'data1subnet' } +$data1nic = New-AzureRmNetworkInterface -ResourceGroupName $resourceGroup -Name "data1nic" -Location $location -SubnetId $data1subnet.Id -PublicIpAddressId $data1pip.Id -NetworkSecurityGroupId $nsg.Id + +$data2subnet = $vnet.Subnets | ?{ $_.Name -eq 'data2subnet' } +$data2nic = New-AzureRmNetworkInterface -ResourceGroupName $resourceGroup -Name "data2nic" -Location $location -SubnetId $data2subnet.Id -PublicIpAddressId $data2pip.Id -NetworkSecurityGroupId $nsg.Id + +# Define a credential object +$name= Read-Host 'Enter Username' +$securePassword = Read-Host 'Enter the password' -AsSecureString +$cred = New-Object System.Management.Automation.PSCredential ($name, $securePassword) + +# Start building the VM configuration +$vmConfig = New-AzureRmVMConfig -VMName $vmName -VMSize $vmSize + +#Create the rest of configuration +$vmConfig = Set-AzureRmVMOperatingSystem -VM $vmConfig -Linux -ComputerName $vmName -Credential $cred +$vmConfig = Set-AzureRmVMSourceImage -VM $vmConfig -PublisherName "a10networks" -Offer "a10-vthunder-adc" -skus "vthunder_500mbps" -Version "latest" +$vmConfig = Set-AzureRmVMPlan -Name "vthunder_500mbps" -Product "a10-vthunder-adc" -Publisher "a10networks" -VM $vmconfig + +# for bootdiag +$vmConfig = Set-AzureRmVMBootDiagnostics -VM $vmconfig -Enable -ResourceGroupName $resourceGroup -StorageAccountName $storageaccount + +#Attach the NIC that are created +$vmConfig = Add-AzureRmVMNetworkInterface -VM $vmConfig -Id $mgmtnic.Id -Primary +$vmConfig = Add-AzureRmVMNetworkInterface -VM $vmConfig -Id $data1nic.Id +$vmConfig = Add-AzureRmVMNetworkInterface -VM $vmConfig -Id $data2nic.Id + +#Creating VM with all configuration +New-AzureRmVM -ResourceGroupName $resourceGroup -Location $location -VM $vmConfig \ No newline at end of file diff --git a/a10vthunder-orchestrator.sln b/a10vthunder-orchestrator.sln new file mode 100644 index 0000000..7504fcb --- /dev/null +++ b/a10vthunder-orchestrator.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31702.278 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "a10vthunder-orchestrator", "a10vthunder-orchestrator\a10vthunder-orchestrator.csproj", "{0E9426F8-B45E-4266-BB6C-7942D1B6B650}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0E9426F8-B45E-4266-BB6C-7942D1B6B650}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E9426F8-B45E-4266-BB6C-7942D1B6B650}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E9426F8-B45E-4266-BB6C-7942D1B6B650}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E9426F8-B45E-4266-BB6C-7942D1B6B650}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C9CA0496-DCB2-49B7-9D70-A6B1DACB282F} + EndGlobalSection +EndGlobal diff --git a/a10vthunder-orchestrator/AnyErrors.cs b/a10vthunder-orchestrator/AnyErrors.cs new file mode 100644 index 0000000..0a7d506 --- /dev/null +++ b/a10vthunder-orchestrator/AnyErrors.cs @@ -0,0 +1,35 @@ +using System; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions; +using Microsoft.Extensions.Logging; + +namespace a10vthunder_orchestrator +{ + public class AnyErrors + { + public virtual bool HasError { get; set; } + + public virtual string ErrorMessage { get; set; } + + public static JobResult ThrowError(ILogger logger, Exception exception, string className, string jobSection) + { + var message = FlattenException(exception); + + logger.LogError($"Error performing {jobSection} in {className} {WindowsUserAnyAgentConstants.StoreTypeName} - {message}"); + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + FailureMessage = message + }; + } + + private static string FlattenException(Exception ex) + { + var returnMessage = ex.Message; + if (ex.InnerException != null) + returnMessage += " - " + FlattenException(ex.InnerException); + + return returnMessage; + } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/AuthRequest.cs b/a10vthunder-orchestrator/Api/Models/AuthRequest.cs new file mode 100644 index 0000000..e26a38a --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/AuthRequest.cs @@ -0,0 +1,11 @@ +namespace a10vthunder_orchestrator.Api.Models +{ + #region JSON Request and Response Classes + + internal class AuthRequest + { + public Credentials Credentials { get; set; } + } + + #endregion +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/AuthResponse.cs b/a10vthunder-orchestrator/Api/Models/AuthResponse.cs new file mode 100644 index 0000000..aae18ad --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/AuthResponse.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class AuthResponse + { + [JsonProperty("signature")] public string Signature { get; set; } + + [JsonProperty("description")] public string Description { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/AuthSignatureResponse.cs b/a10vthunder-orchestrator/Api/Models/AuthSignatureResponse.cs new file mode 100644 index 0000000..fba6575 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/AuthSignatureResponse.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class AuthSignatureResponse + { + [JsonProperty("authresponse")] public AuthResponse Response { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/Credentials.cs b/a10vthunder-orchestrator/Api/Models/Credentials.cs new file mode 100644 index 0000000..08d4b57 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/Credentials.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + internal class Credentials + { + [JsonProperty("username")] public string Username { get; set; } + + [JsonProperty("password")] public string Password { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/DeleteCertBaseRequest.cs b/a10vthunder-orchestrator/Api/Models/DeleteCertBaseRequest.cs new file mode 100644 index 0000000..f6771c9 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/DeleteCertBaseRequest.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class DeleteCertBaseRequest + { + [JsonProperty("delete", NullValueHandling = NullValueHandling.Ignore)] + public DeleteCertRequest DeleteCert { get; set; } + + } +} diff --git a/a10vthunder-orchestrator/Api/Models/DeleteCertRequest.cs b/a10vthunder-orchestrator/Api/Models/DeleteCertRequest.cs new file mode 100644 index 0000000..d5c2b90 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/DeleteCertRequest.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class DeleteCertRequest + { + [JsonProperty("cert-name")] + public string CertName { get; set; } + + [JsonProperty("private-key",NullValueHandling=NullValueHandling.Ignore)] + public string PrivateKey { get; set; } + } +} diff --git a/a10vthunder-orchestrator/Api/Models/Operation.cs b/a10vthunder-orchestrator/Api/Models/Operation.cs new file mode 100644 index 0000000..047f07c --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/Operation.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class Operation + { + [JsonProperty("oper")] public SslCertificateCollection Oper { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/SslCert.cs b/a10vthunder-orchestrator/Api/Models/SslCert.cs new file mode 100644 index 0000000..5e5daf0 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/SslCert.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class SslCert + { + [JsonProperty("certificate-type")] public string CertificateType { get; set; } + + [JsonProperty("action")] public string Action { get; set; } + + [JsonProperty("file")] public string File { get; set; } + + [JsonProperty("file-handle")] public string FileHandle { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/SslCertKey.cs b/a10vthunder-orchestrator/Api/Models/SslCertKey.cs new file mode 100644 index 0000000..5168f7f --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/SslCertKey.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class SslCertKey + { + [JsonProperty("action")] public string Action { get; set; } + + [JsonProperty("file")] public string File { get; set; } + + [JsonProperty("file-handle")] public string FileHandle { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/SslCertificate.cs b/a10vthunder-orchestrator/Api/Models/SslCertificate.cs new file mode 100644 index 0000000..c4067b5 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/SslCertificate.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class SslCertificate + { + [JsonProperty("name")] public string Name { get; set; } + + [JsonProperty("type")] public string Type { get; set; } + + [JsonProperty("serial")] public string Serial { get; set; } + + [JsonProperty("notbefore")] public string NotBefore { get; set; } + + [JsonProperty("notafter")] public string NotAfter { get; set; } + + [JsonProperty("common-name")] public string CommonName { get; set; } + + [JsonProperty("organization")] public string Organization { get; set; } + + [JsonProperty("subject")] public string Subject { get; set; } + + [JsonProperty("issuer")] public string Issuer { get; set; } + + [JsonProperty("status")] public string Status { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/SslCertificateCollection.cs b/a10vthunder-orchestrator/Api/Models/SslCertificateCollection.cs new file mode 100644 index 0000000..7f00806 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/SslCertificateCollection.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class SslCertificateCollection + { + [JsonProperty("ssl-certs")] public SslCertificate[] SslCertificates { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/SslCertificateRequest.cs b/a10vthunder-orchestrator/Api/Models/SslCertificateRequest.cs new file mode 100644 index 0000000..9e47ab5 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/SslCertificateRequest.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class SslCertificateRequest + { + [JsonProperty("ssl-cert")] public SslCert SslCertificate { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/SslCollectionResponse.cs b/a10vthunder-orchestrator/Api/Models/SslCollectionResponse.cs new file mode 100644 index 0000000..bcd7fda --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/SslCollectionResponse.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class SslCollectionResponse + { + [JsonProperty("ssl-cert")] public Operation SslCertificate { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/SslKeyRequest.cs b/a10vthunder-orchestrator/Api/Models/SslKeyRequest.cs new file mode 100644 index 0000000..996e415 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/SslKeyRequest.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api.Models +{ + public class SslKeyRequest + { + [JsonProperty("ssl-key")] public SslCertKey SslKey { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/CertManager.cs b/a10vthunder-orchestrator/CertManager.cs new file mode 100644 index 0000000..a09fbd5 --- /dev/null +++ b/a10vthunder-orchestrator/CertManager.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using a10vthunder_orchestrator.Api; +using a10vthunder_orchestrator.Api.Models; +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions; +using Microsoft.Extensions.Logging; + +namespace a10vthunder_orchestrator +{ + public class CertManager + { + private ILogger Logger { get; } + + public CertManager() + { + Logger = LogHandler.GetClassLogger(); + } + + public virtual InventoryResult GetCert(ApiClient apiClient, string certName) + { + try + { + return InventoryResult(apiClient, certName); + } + catch (Exception ex) + { + Logger.LogError($"Error in CertManager.GetCerts: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public virtual InventoryResult GetCerts(ApiClient apiClient) + { + try + { + return InventoryResult(apiClient); + } + catch (Exception ex) + { + Logger.LogError($"Error in CertManager.GetCerts: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + protected virtual SslCollectionResponse CertificateCollection { get; set; } + + protected virtual string CertificateDetails { get; set; } + + public virtual InventoryResult InventoryResult(ApiClient apiClient, string certName = "") + { + try + { + ILogger logger = LogHandler.GetClassLogger(); + + var result = new InventoryResult(); + var error = new AnyErrors {HasError = false}; + + logger.LogTrace("GetCerts"); + + + CertificateCollection = + certName == "" ? apiClient.GetCertificates() : apiClient.GetCertificates(certName); + + var inventoryItems = new List(); + + logger.LogTrace("Start loop"); + + if (CertificateCollection != null) + foreach (var cc in CertificateCollection.SslCertificate.Oper.SslCertificates) + if (!string.IsNullOrEmpty(cc.Name)) + { + logger.LogTrace($"Looping through Certificate Store files: {cc.Name}"); + + var privateKeyEntry = cc.Type == "certificate/key"; + + try + { + CertificateDetails = apiClient.GetCertificate(cc.Name); + + //check this is a valid cert, if not fall to the errors + var cert = new X509Certificate2(Encoding.UTF8.GetBytes(CertificateDetails)); + + logger.LogTrace($"Add to list: {cc.Name}"); + if (cert.Thumbprint != null) + inventoryItems.Add( + new CurrentInventoryItem + { + Certificates = new[] + {CertificateDetails}, + Alias = cc.Name, + PrivateKeyEntry = privateKeyEntry, + ItemStatus = OrchestratorInventoryItemStatus.Unknown, + UseChainLevel = true + }); + } + catch (Exception ex) + { + logger.LogError($"Certificate not retrievable: Error on {cc.Name}: {ex.Message}"); + error.ErrorMessage = ex.Message; + error.HasError = true; + } + } + + + result.Errors = error; + result.InventoryList = inventoryItems; + + return result; + } + catch (Exception ex) + { + Logger.LogError($"Error in CertManager.InventoryResult: {LogHandler.FlattenException(ex)}"); + throw; + } + } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Constants.cs b/a10vthunder-orchestrator/Constants.cs new file mode 100644 index 0000000..ab64a03 --- /dev/null +++ b/a10vthunder-orchestrator/Constants.cs @@ -0,0 +1,15 @@ +namespace a10vthunder_orchestrator +{ + static class WindowsUserAnyAgentConstants + { + public const string StoreTypeName = "vThunder"; + } + + static class JobTypes + { + public const string Create = "Create"; + public const string Discovery = "Discovery"; + public const string Inventory = "Inventory"; + public const string Management = "Management"; + } +} diff --git a/a10vthunder-orchestrator/Exceptions/InvalidInventoryInvokeException.cs b/a10vthunder-orchestrator/Exceptions/InvalidInventoryInvokeException.cs new file mode 100644 index 0000000..d393678 --- /dev/null +++ b/a10vthunder-orchestrator/Exceptions/InvalidInventoryInvokeException.cs @@ -0,0 +1,11 @@ +using System; + +namespace a10vthunder_orchestrator.Exceptions +{ + internal class InvalidInventoryInvokeException : Exception + { + public InvalidInventoryInvokeException() : base("SubmitInventory.Invoke returned false") + { + } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Exceptions/UnsupportedOperationException.cs b/a10vthunder-orchestrator/Exceptions/UnsupportedOperationException.cs new file mode 100644 index 0000000..c80eb1d --- /dev/null +++ b/a10vthunder-orchestrator/Exceptions/UnsupportedOperationException.cs @@ -0,0 +1,11 @@ +using System; + +namespace a10vthunder_orchestrator.Exceptions +{ + internal class UnSupportedOperationException : Exception + { + public UnSupportedOperationException() : base("Unsupported Operation, only Add, Remove are supported") + { + } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/InventoryResult.cs b/a10vthunder-orchestrator/InventoryResult.cs new file mode 100644 index 0000000..70fb23d --- /dev/null +++ b/a10vthunder-orchestrator/InventoryResult.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Keyfactor.Orchestrators.Extensions; + +namespace a10vthunder_orchestrator +{ + public class InventoryResult + { + public virtual AnyErrors Errors { get; set; } + + public virtual List InventoryList { get; set; } + } +} diff --git a/a10vthunder-orchestrator/JobAttribute.cs b/a10vthunder-orchestrator/JobAttribute.cs new file mode 100644 index 0000000..035ac32 --- /dev/null +++ b/a10vthunder-orchestrator/JobAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace a10vthunder_orchestrator +{ + [AttributeUsage(AttributeTargets.Class)] + public class JobAttribute : Attribute + { + // ReSharper disable once InconsistentNaming + private string jobClass { get; set; } + + public JobAttribute(string jobClass) + { + this.jobClass = jobClass; + } + + public virtual string JobClass => jobClass; + } +} diff --git a/a10vthunder-orchestrator/Jobs/Inventory.cs b/a10vthunder-orchestrator/Jobs/Inventory.cs new file mode 100644 index 0000000..d144f8c --- /dev/null +++ b/a10vthunder-orchestrator/Jobs/Inventory.cs @@ -0,0 +1,95 @@ +using System; +using a10vthunder_orchestrator.Api; +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Jobs +{ + public class Inventory : IInventoryJobExtension + { + private readonly ILogger _logger; + + public Inventory(ILogger logger) + { + _logger = logger; + } + + protected internal virtual InventoryResult Result { get; set; } + protected internal virtual CertManager CertificateManager { get; set; } + protected internal virtual ApiClient ApiClient { get; set; } + protected internal virtual string Protocol { get; set; } + protected internal virtual bool AllowInvalidCert { get; set; } + protected internal virtual bool ReturnValue { get; set; } + public string ExtensionName => "VThunderU"; + + public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate submitInventory) + { + _logger.MethodEntry(); + + dynamic properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); + Protocol = properties != null && + (properties.protocol == null || string.IsNullOrEmpty(properties.protocol.Value)) + ? "https" + : properties?.protocol.Value; + AllowInvalidCert = + properties?.allowInvalidCert == null || string.IsNullOrEmpty(properties.allowInvalidCert.Value) + ? false + : bool.Parse(properties.allowInvalidCert.Value); + + using (ApiClient = new ApiClient(config.ServerUsername, config.ServerPassword, + $"{Protocol}://{config.CertificateStoreDetails.ClientMachine.Trim()}", AllowInvalidCert)) + { + ApiClient.Logon(); + try + { + _logger.LogTrace("Parse: Certificate Inventory: " + config.CertificateStoreDetails.StorePath); + _logger.LogTrace( + $"Certificate Store: {config.CertificateStoreDetails.ClientMachine} {config.CertificateStoreDetails.StorePath}"); + + _logger.LogTrace("Entering A10 VThunder DataPower: Certificate Inventory"); + _logger.LogTrace( + $"Entering processJob for Certificate Store: {config.CertificateStoreDetails.ClientMachine} {config.CertificateStoreDetails.StorePath}"); + CertificateManager = new CertManager(); + Result = CertificateManager.GetCerts(ApiClient); + _logger.LogTrace($"Got {Result.InventoryList.Count} Certs from Inventory"); + + ReturnValue = submitInventory.Invoke(Result.InventoryList); + _logger.LogTrace("Invoked Inventory"); + _logger.MethodExit(); + + if (ReturnValue == false) + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = "Error Invoking Inventory" + }; + + if (Result.Errors.HasError) + return new JobResult + { + JobHistoryId = config.JobHistoryId, + FailureMessage = + $"Inventory had issues retrieving some certificates: {Result.Errors.ErrorMessage}", + Result = OrchestratorJobStatusJobResult.Warning + }; + + return new JobResult + {JobHistoryId = config.JobHistoryId, Result = OrchestratorJobStatusJobResult.Success}; + } + catch (Exception e) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = $"Inventory Error Unknown {LogHandler.FlattenException(e)}" + }; + } + } + } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Jobs/Management.cs b/a10vthunder-orchestrator/Jobs/Management.cs new file mode 100644 index 0000000..a3b6050 --- /dev/null +++ b/a10vthunder-orchestrator/Jobs/Management.cs @@ -0,0 +1,297 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using a10vthunder_orchestrator.Api; +using a10vthunder_orchestrator.Api.Models; +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; + +namespace a10vthunder_orchestrator.Jobs +{ + public class Management : IManagementJobExtension + { + protected internal static Func Pemify = ss => + ss.Length <= 64 ? ss : ss.Substring(0, 64) + "\n" + Pemify(ss.Substring(64)); + + private readonly ILogger _logger; + + public Management(ILogger logger) + { + _logger = logger; + } + + protected internal virtual string Protocol { get; set; } + protected internal virtual bool AllowInvalidCert { get; set; } + protected internal virtual ApiClient ApiClient { get; set; } + protected internal virtual CertManager CertManager { get; set; } + protected internal virtual InventoryResult InventoryResult { get; set; } + protected internal virtual bool ExistingCert { get; set; } + protected internal virtual string CertStart { get; set; } = "-----BEGIN CERTIFICATE-----\n"; + protected internal virtual string CertEnd { get; set; } = "\n-----END CERTIFICATE-----"; + protected internal virtual string Alias { get; set; } + public string ExtensionName => "VThunderU"; + + public JobResult ProcessJob(ManagementJobConfiguration config) + { + _logger.MethodEntry(); + _logger.LogTrace($"config settings: {JsonConvert.SerializeObject(config)}"); + dynamic properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); + _logger.LogTrace($"properties: {JsonConvert.SerializeObject(properties)}"); + Protocol = properties?.protocol == null || string.IsNullOrEmpty(properties.protocol.Value) + ? "https" + : properties.protocol.Value; + AllowInvalidCert = + properties?.allowInvalidCert == null || string.IsNullOrEmpty(properties.allowInvalidCert.Value) + ? false + : bool.Parse(properties.allowInvalidCert.Value); + + CertManager = new CertManager(); + _logger.LogTrace($"Ending Management Constructor Protocol is {Protocol}"); + + using (ApiClient = new ApiClient(config.ServerUsername, config.ServerPassword, + $"{Protocol}://{config.CertificateStoreDetails.ClientMachine.Trim()}", AllowInvalidCert)) + { + _logger.LogTrace("Entering APIClient Using clause"); + if (string.IsNullOrEmpty(config.JobCertificate.Alias)) + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = "Management Missing Alias/Overwrite, Operation Cannot Be Completed" + }; + + ApiClient.Logon(); + InventoryResult = CertManager.GetCert(ApiClient, config.JobCertificate.Alias); + ExistingCert = InventoryResult != null && InventoryResult?.InventoryList?.Count == 1; + + switch (config.OperationType) + { + case CertStoreOperationType.Add: + try + { + if (ExistingCert) + { + if (config.Overwrite) + { + _logger.LogTrace($"Starting Replace Job for {config.JobCertificate.Alias}"); + Replace(config, InventoryResult, ApiClient); + _logger.LogTrace($"Finishing Replace Job for {config.JobCertificate.Alias}"); + } + else + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = "You must use the overwrite flag to replace an existing certificate." + }; + } + } + + if(!ExistingCert) + { + _logger.LogTrace($"Starting Add Job for {config.JobCertificate.Alias}"); + Add(config, ApiClient); + _logger.LogTrace($"Finishing Add Job for {config.JobCertificate.Alias}"); + } + } + catch (Exception e) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = $"Error Adding Certificate {LogHandler.FlattenException(e)}" + }; + } + + break; + case CertStoreOperationType.Remove: + try + { + _logger.LogTrace($"Starting Remove Job for {config.JobCertificate.Alias}"); + Remove(config, InventoryResult, ApiClient); + _logger.LogTrace($"Finishing Remove Job for {config.JobCertificate.Alias}"); + } + catch (Exception e) + { + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = $"Error Removing Certificate {LogHandler.FlattenException(e)}" + }; + } + + break; + default: + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = "Unsupported Operation, only Add, Remove and Replace are supported" + }; + } + + _logger.LogTrace($"Finishing Process Job for {config.JobCertificate.Alias}"); + return new JobResult + {JobHistoryId = config.JobHistoryId, Result = OrchestratorJobStatusJobResult.Success}; + } + } + + protected internal virtual void Replace(ManagementJobConfiguration config, InventoryResult inventoryResult, + ApiClient apiClient) + { + try + { + Remove(config, inventoryResult, apiClient); + Add(config, apiClient); + } + catch (Exception ex) + { + _logger.LogError($"Error in Management.Replace: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + protected internal virtual void Remove(ManagementJobConfiguration configInfo, InventoryResult inventoryResult, + ApiClient apiClient) + { + try + { + _logger.LogTrace($"Start Delete the {configInfo.JobCertificate.Alias} Private Key"); + DeleteCertBaseRequest deleteKeyRoot; + if (inventoryResult.InventoryList[0].PrivateKeyEntry) + deleteKeyRoot = new DeleteCertBaseRequest + { + DeleteCert = new DeleteCertRequest + { + CertName = configInfo.JobCertificate.Alias, + PrivateKey = configInfo.JobCertificate.Alias + } + }; + else + deleteKeyRoot = new DeleteCertBaseRequest + { + DeleteCert = new DeleteCertRequest + { + CertName = configInfo.JobCertificate.Alias + } + }; + + apiClient.RemoveCertificate(deleteKeyRoot); + _logger.LogTrace($"Successful Delete of the {configInfo.JobCertificate.Alias} Private Key"); + } + catch (Exception ex) + { + _logger.LogError($"Error in Management.Remove: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + protected internal virtual void Add(ManagementJobConfiguration configInfo, ApiClient apiClient) + { + try + { + _logger.LogTrace($"Entering Add Function for {configInfo.JobCertificate.Alias}"); + var privateKeyString = ""; + string certPem; + + if (!string.IsNullOrEmpty(configInfo.JobCertificate.PrivateKeyPassword)) + { + _logger.LogTrace( + $"Pfx Password exists getting Private Key string for {configInfo.JobCertificate.Alias}"); + var certData = Convert.FromBase64String(configInfo.JobCertificate.Contents); + var store = new Pkcs12Store(new MemoryStream(certData), + configInfo.JobCertificate.PrivateKeyPassword.ToCharArray()); + + using (var memoryStream = new MemoryStream()) + { + using (TextWriter streamWriter = new StreamWriter(memoryStream)) + { + var pemWriter = new PemWriter(streamWriter); + _logger.LogTrace($"Getting Public Key for {configInfo.JobCertificate.Alias}"); + Alias = store.Aliases.Cast().SingleOrDefault(a => store.IsKeyEntry(a)); + var publicKey = store.GetCertificate(Alias).Certificate.GetPublicKey(); + _logger.LogTrace($"Getting Private Key for {configInfo.JobCertificate.Alias}"); + var privateKey = store.GetKey(Alias).Key; + var keyPair = new AsymmetricCipherKeyPair(publicKey, privateKey); + _logger.LogTrace($"Writing Private Key for {configInfo.JobCertificate.Alias}"); + pemWriter.WriteObject(keyPair.Private); + streamWriter.Flush(); + privateKeyString = Encoding.ASCII.GetString(memoryStream.GetBuffer()).Trim() + .Replace("\r", "").Replace("\0", ""); + memoryStream.Close(); + streamWriter.Close(); + _logger.LogTrace($"Private Key String Retrieved for {configInfo.JobCertificate.Alias}"); + } + } + + // Extract server certificate + var beginCertificate = "-----BEGIN CERTIFICATE-----\n"; + var endCertificate = "\n-----END CERTIFICATE-----"; + + _logger.LogTrace($"Start getting Server Certificate for {configInfo.JobCertificate.Alias}"); + certPem = beginCertificate + + Pemify(Convert.ToBase64String(store.GetCertificate(Alias).Certificate.GetEncoded())) + + endCertificate; + _logger.LogTrace($"Finished getting Server Certificate for {configInfo.JobCertificate.Alias}"); + } + else + { + _logger.LogTrace($"No Private Key get Cert Pem {configInfo.JobCertificate.Alias}"); + certPem = CertStart + Pemify(configInfo.JobCertificate.Contents) + CertEnd; + } + + _logger.LogTrace($"Creating Cert API Add Request for {configInfo.JobCertificate.Alias}"); + var sslCertRequest = new SslCertificateRequest + { + SslCertificate = new SslCert + { + Action = "import", + CertificateType = "pem", + File = configInfo.JobCertificate.Alias.Replace(".pem", ".pem"), + FileHandle = configInfo.JobCertificate.Alias.Replace(".pem", ".pem") + } + }; + + _logger.LogTrace($"Making API Call to Add Certificate For {configInfo.JobCertificate.Alias}"); + apiClient.AddCertificate(sslCertRequest, certPem); + _logger.LogTrace($"Finished API Call to Add Certificate For {configInfo.JobCertificate.Alias}"); + + if (!string.IsNullOrEmpty(configInfo.JobCertificate.PrivateKeyPassword)) + { + _logger.LogTrace($"Creating Key API Add Request for {configInfo.JobCertificate.Alias}"); + var sslKeyRequest = new SslKeyRequest + { + SslKey = new SslCertKey + { + Action = "import", + File = configInfo.JobCertificate.Alias.Replace(".pem", ".pem"), + FileHandle = configInfo.JobCertificate.Alias.Replace(".pem", ".pem") + } + }; + + _logger.LogTrace($"Making Add Key API Call for {configInfo.JobCertificate.Alias}"); + apiClient.AddPrivateKey(sslKeyRequest, privateKeyString); + _logger.LogTrace($"Finished Add Key API Call for {configInfo.JobCertificate.Alias}"); + } + + _logger.LogTrace($"Starting Log Off for Add {configInfo.JobCertificate.Alias}"); + _logger.LogTrace($"Finished Log Off for Add {configInfo.JobCertificate.Alias}"); + } + catch (Exception ex) + { + _logger.LogError($"Error in Management.Add: {LogHandler.FlattenException(ex)}"); + throw; + } + } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/a10vthunder-orchestrator.csproj b/a10vthunder-orchestrator/a10vthunder-orchestrator.csproj new file mode 100644 index 0000000..02e9deb --- /dev/null +++ b/a10vthunder-orchestrator/a10vthunder-orchestrator.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + true + a10vthunder_orchestrator + + + + + + + + + + + + + + diff --git a/a10vthunder-orchestrator/api/ApiClient.cs b/a10vthunder-orchestrator/api/ApiClient.cs new file mode 100644 index 0000000..891fd87 --- /dev/null +++ b/a10vthunder-orchestrator/api/ApiClient.cs @@ -0,0 +1,477 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using a10vthunder_orchestrator.Api.Models; +using Keyfactor.Logging; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace a10vthunder_orchestrator.Api +{ + public sealed class ApiClient : IDisposable + { + private static readonly Encoding Encoding = Encoding.UTF8; + + #region Constructors + + public ApiClient(string user, string pass, string baseUrl, bool allowInvalidCert) + { + Logger = LogHandler.GetClassLogger(); + BaseUrl = baseUrl; + UserId = user; + Password = pass; + AllowInvalidCert = allowInvalidCert; + } + + #endregion + + public string AuthenticationSignature { get; set; } + public string BaseUrl { get; set; } + public bool AllowInvalidCert { get; set; } + public string UserId { get; set; } + public string Password { get; set; } + private ILogger Logger { get; } + + #region Interface Implementation + + public void Dispose() + { + LogOff(); + } + + #endregion + + #region Class Methods + + public void Logon() + { + Logger.MethodEntry(); + var authRequest = new AuthRequest + {Credentials = new Credentials {Username = UserId, Password = Password}}; + var strRequest = JsonConvert.SerializeObject(authRequest); + try + { + Logger.LogTrace($"Logging Login Request JSON: {strRequest}"); + var strResponse = ApiRequestString("POST", "/axapi/v3/auth", "POST", strRequest, false, false); + Logger.LogTrace($"Logging Login Response JSON: {strResponse}"); + var authSignatureResponse = JsonConvert.DeserializeObject(strResponse); + Logger.LogTrace($"Auth Signature Response: {JsonConvert.SerializeObject(authSignatureResponse)}"); + AuthenticationSignature = authSignatureResponse?.Response.Signature; + Logger.LogTrace($"Auth Signature: {AuthenticationSignature}"); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError($"Error Authenticating: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public bool LogOff() + { + try + { + Logger.MethodEntry(); + ApiRequestString("POST", "/axapi/v3/logoff", "POST", "", false, true); + Logger.MethodExit(); + return true; + } + catch (Exception ex) + { + Logger.LogError($"Error Logging Off: {LogHandler.FlattenException(ex)}"); + return false; + } + } + + public void AddCertificate(SslCertificateRequest sslCertRequest, string importCertificate) + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"Ssl Certificate Request: {JsonConvert.SerializeObject(sslCertRequest)}"); + Logger.LogTrace($"importCertificate: {JsonConvert.SerializeObject(importCertificate)}"); + var certData = Encoding.ASCII.GetBytes(importCertificate); + Logger.LogTrace("Got Cert Data Adding Certificate..."); + AddCertificate(sslCertRequest, certData); + Logger.LogTrace("Added Certificate..."); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError( + $"Error In ApiClient.AddCertificate(SslCertificateRequest sslCertRequest, string importCertificate): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public void AddCertificate(SslCertificateRequest sslCertRequest, byte[] certData) + { + try + { + Logger.MethodEntry(); + var strRequest = JsonConvert.SerializeObject(sslCertRequest); + Logger.LogTrace($"sslCertRequest: {JsonConvert.SerializeObject(strRequest)}"); + var requestArray = Encoding.ASCII.GetBytes(strRequest); + Logger.LogTrace("Got requestArray..."); + // Generate post objects + var postParameters = new Dictionary + { + {"json", new FileParameter(requestArray, "a10.json", "application/json")}, + { + "file", + new FileParameter(certData, sslCertRequest.SslCertificate.File, "application/octet-stream") + } + }; + + // Create request and receive response + var userAgent = "Keyfactor Agent"; + var webResponse = MultipartFormDataPost("/axapi/v3/file/ssl-cert", userAgent, postParameters); + Logger.LogTrace("Got webResponse..."); + using var responseReader = new StreamReader(webResponse.GetResponseStream() ?? Stream.Null); + responseReader.ReadToEnd(); + webResponse.Close(); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError( + $"Error In ApiClient.AddCertificate(SslCertificateRequest sslCertRequest, byte[] certData): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public bool AddPrivateKey(SslKeyRequest sslKeyRequest, string importCertificateKey) + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"sslKeyRequest: {JsonConvert.SerializeObject(sslKeyRequest)}"); + Logger.LogTrace($"importCertificateKey: {importCertificateKey}"); + var keyArray = Encoding.ASCII.GetBytes(importCertificateKey); + Logger.LogTrace("Got keyArray..."); + AddPrivateKey(sslKeyRequest, keyArray); + Logger.LogTrace("Added PrivateKey..."); + return true; + } + catch (Exception ex) + { + Logger.LogError( + $"Error In AddPrivateKey(SslKeyRequest sslKeyRequest, string importCertificateKey): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public void AddPrivateKey(SslKeyRequest sslKeyRequest, byte[] keyArray) + { + try + { + Logger.MethodEntry(); + var strRequest = JsonConvert.SerializeObject(sslKeyRequest); + Logger.LogTrace($"sslKeyRequest: {JsonConvert.SerializeObject(sslKeyRequest)}"); + var requestArray = Encoding.ASCII.GetBytes(strRequest); + + // Generate post objects + var postParameters = new Dictionary + { + {"json", new FileParameter(requestArray, "a10.json", "application/json")}, + {"file", new FileParameter(keyArray, sslKeyRequest.SslKey.File, "application/octet-stream")} + }; + + // Create request and receive response + var userAgent = "Keyfactor Agent"; + var webResponse = MultipartFormDataPost("/axapi/v3/file/ssl-key", userAgent, postParameters); + Logger.LogTrace("Got webResponse..."); + + // Process response + using var responseReader = new StreamReader(webResponse.GetResponseStream() ?? Stream.Null); + responseReader.ReadToEnd(); + webResponse.Close(); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError( + $"Error In AddPrivateKey(SslKeyRequest sslKeyRequest, byte[] keyArray): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public SslCollectionResponse GetCertificates(string certName = "") + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"certName: {certName}"); + var strResponse = ApiRequestString("GET", + certName.Length == 0 + ? "/axapi/v3/slb/ssl-cert/oper" + : $"/axapi/v3/slb/ssl-cert/oper?name={certName}", "GET", "", false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + var sslColResponse = JsonConvert.DeserializeObject(strResponse); + Logger.LogTrace($"sslColResponse: {JsonConvert.SerializeObject(sslColResponse)}"); + Logger.MethodExit(); + return sslColResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In GetCertificates(string certName): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public string GetCertificate(string certificateName) + { + try + { + Logger.MethodEntry(); + var strResponse = ApiRequestString("GET", $"/axapi/v3/file/ssl-cert/{certificateName}", "GET", "", + false, + true); + Logger.LogTrace($"strResponse: {strResponse}"); + Logger.MethodExit(); + return strResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In GetCertificate(string certificateName): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public void RemoveCertificate(DeleteCertBaseRequest deleteCertRoot) + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"deleteCertRoot: {JsonConvert.SerializeObject(deleteCertRoot)}"); + ApiRequestString("POST", "/axapi/v3/pki/delete", "POST", JsonConvert.SerializeObject(deleteCertRoot), + false, true); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError( + $"Error In RemoveCertificate(DeleteCertBaseRequest deleteCertRoot): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public HttpWebRequest CreateRequest(string baseUrl, string postUrl) + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"baseUrl: {baseUrl} postUrl: {postUrl}"); + var objRequest = (HttpWebRequest) WebRequest.Create(BaseUrl + postUrl); + Logger.MethodExit(); + return objRequest; + } + catch (Exception ex) + { + Logger.LogError( + $"Error In CreateRequest(string baseUrl, string postUrl): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public HttpWebResponse GetResponse(HttpWebRequest request) + { + try + { + Logger.MethodEntry(); + Logger.MethodExit(); + return (HttpWebResponse) request.GetResponse(); + } + catch (Exception ex) + { + Logger.LogError($"Error In GetResponse(HttpWebRequest request): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public string ApiRequestString(string strCall, string strPostUrl, string strMethod, + string strQueryString, + bool bWrite, bool bUseToken) + { + try + { + Logger.MethodEntry(); + var objRequest = CreateRequest(BaseUrl, strPostUrl); + Logger.LogTrace( + $"Request Object Created... method will be {strMethod} postURL will be {strPostUrl} query string will be {strQueryString}"); + objRequest.Method = strMethod; + objRequest.ContentType = "application/json"; + Logger.LogTrace($"Use Token {bUseToken}"); + Logger.LogTrace($"AuthenticationSignature {AuthenticationSignature}"); + if (bUseToken) + objRequest.Headers.Add("Authorization", "A10 " + AuthenticationSignature); + + if (!string.IsNullOrEmpty(strQueryString) && strMethod == "POST") + { + var postBytes = Encoding.UTF8.GetBytes(strQueryString); + Logger.LogTrace($"postBytes.Length {postBytes.Length}"); + objRequest.ContentLength = postBytes.Length; + //This is for testing on an Azure VM with an invalid certificate + if (AllowInvalidCert) + ServicePointManager.ServerCertificateValidationCallback = (a, b, c, d) => true; + using var requestStream = objRequest.GetRequestStream(); + requestStream.Write(postBytes, 0, postBytes.Length); + requestStream.Close(); + } + + Logger.LogTrace($"AllowInvalidCert {AllowInvalidCert}"); + //This is for testing on an Azure VM with an invalid certificate + if (AllowInvalidCert) + ServicePointManager.ServerCertificateValidationCallback = (a, b, c, d) => true; + var objResponse = GetResponse(objRequest); + Logger.LogTrace("Got Response"); + using var strReader = new StreamReader(objResponse.GetResponseStream() ?? Stream.Null); + var strResponse = strReader.ReadToEnd(); + Logger.MethodExit(); + return strResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In ApiRequestString: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, + Dictionary postParameters) + { + try + { + Logger.MethodEntry(); + var boundary = $"{Guid.NewGuid():N}"; + Logger.LogTrace($"boundary {boundary}"); + var formDataBoundary = $"------------------------{boundary}"; + Logger.LogTrace($"formDataBoundary {formDataBoundary}"); + var contentType = "multipart/form-data; boundary=" + formDataBoundary; + Logger.LogTrace($"contentType {contentType}"); + var formData = GetMultipartFormData(postParameters, boundary); + Logger.LogTrace("Got formData"); + Logger.MethodExit(); + return PostForm(postUrl, userAgent, contentType, formData); + } + catch (Exception ex) + { + Logger.LogError($"Error In MultipartFormDataPost: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + private HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, + byte[] formData) + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"postUrl {postUrl}"); + Logger.LogTrace($"userAgent {userAgent}"); + Logger.LogTrace($"contentType {contentType}"); + var request = CreateRequest(BaseUrl, postUrl); + if (request == null) throw new NullReferenceException("request is not a http request"); + Logger.LogTrace("Request Created..."); + // Set up the request properties. + request.Method = "POST"; + request.ContentType = contentType; + request.UserAgent = userAgent; + request.ContentLength = formData.Length; + Logger.LogTrace($"ContentLength {request.ContentLength}"); + Logger.LogTrace($"AuthenticationSignature {AuthenticationSignature}"); + request.Headers.Add("Authorization", "A10 " + AuthenticationSignature); + //This is for testing on an Azure VM with an invalid certificate + if (AllowInvalidCert) + ServicePointManager.ServerCertificateValidationCallback = (a, b, c, d) => true; + // Send the form data to the request. + using (var requestStream = request.GetRequestStream()) + { + requestStream.Write(formData, 0, formData.Length); + requestStream.Close(); + } + + Logger.LogTrace($"AllowInvalidCert {AllowInvalidCert}"); + //This is for testing on an Azure VM with an invalid certificate + if (AllowInvalidCert) + ServicePointManager.ServerCertificateValidationCallback = (a, b, c, d) => true; + Logger.MethodExit(); + return request.GetResponse() as HttpWebResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In PostForm: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + private byte[] GetMultipartFormData(Dictionary postParameters, string boundary) + { + try + { + Logger.MethodEntry(); + using Stream formDataStream = new MemoryStream(); + var needsClrf = false; + + foreach (var param in postParameters) + { + // Thanks to feedback from comment-ers, add a CRLF to allow multiple parameters to be added. + // Skip it on the first parameter, add it to subsequent parameters. + if (needsClrf) + formDataStream.Write(Encoding.GetBytes("\r\n"), 0, Encoding.GetByteCount("\r\n")); + + needsClrf = true; + + if (param.Value is FileParameter fileToUpload) + { + // Add just the first part of this param, since we will write the file data directly to the Stream + + var header = + $"--------------------------{boundary}\r\nContent-Disposition: form-data; name=\"{param.Key}\"; filename=\"{fileToUpload.FileName ?? param.Key}\"\r\nContent-Type: {fileToUpload.ContentType ?? "application/octet-stream"}\r\n\r\n"; + + formDataStream.Write(Encoding.GetBytes(header), 0, Encoding.GetByteCount(header)); + + // Write the file data directly to the Stream, rather than serializing it to a string. + formDataStream.Write(fileToUpload.File, 0, fileToUpload.File.Length); + } + } + + // Add the end of the request. Start with a newline + var footer = "\r\n--------------------------" + boundary + "--\r\n"; + formDataStream.Write(Encoding.GetBytes(footer), 0, Encoding.GetByteCount(footer)); + + // Dump the Stream into a byte[] + formDataStream.Position = 0; + var formData = new byte[formDataStream.Length]; + // ReSharper disable once MustUseReturnValue + formDataStream.Read(formData, 0, formData.Length); + formDataStream.Close(); + Logger.MethodExit(); + return formData; + } + catch (Exception ex) + { + Logger.LogError($"Error In GetMultipartFormData: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public class FileParameter + { + public FileParameter(byte[] file, string filename, string contentType) + { + File = file; + FileName = filename; + ContentType = contentType; + } + + public byte[] File { get; set; } + public string FileName { get; set; } + public string ContentType { get; set; } + } + + #endregion + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/manifest.json b/a10vthunder-orchestrator/manifest.json new file mode 100644 index 0000000..ef7f4ad --- /dev/null +++ b/a10vthunder-orchestrator/manifest.json @@ -0,0 +1,14 @@ +{ + "extensions": { + "Keyfactor.Orchestrators.Extensions.IOrchestratorJobExtension": { + "CertStores.vThunderU.Inventory": { + "assemblypath": "a10vthunder-orchestrator.dll", + "TypeFullName": "a10vthunder_orchestrator.Jobs.Inventory" + }, + "CertStores.vThunderU.Management": { + "assemblypath": "a10vthunder-orchestrator.dll", + "TypeFullName": "a10vthunder_orchestrator.Jobs.Management" + } + } + } +} \ No newline at end of file diff --git a/integration-manifest.json b/integration-manifest.json new file mode 100644 index 0000000..d66f42e --- /dev/null +++ b/integration-manifest.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", + "integration_type": "orchestrator", + "name": "a10vThunder", + "status": "Pilot", + "description": "A10 vThunder AnyAgent allows an organization to inventory and deploy certificates in any domain that the appliance services. The AnyAgent deploys the appropriate files (.cer, .pem) within the defined directories and also performs and Inventory on the Items." +} diff --git a/readme_source.md b/readme_source.md new file mode 100644 index 0000000..6b41cbd --- /dev/null +++ b/readme_source.md @@ -0,0 +1,136 @@ +**A10 Networks vThunder Orchestrator** + +**Overview** + +A10 vThunder AnyAgent allows an organization to inventory and deploy certificates in any domain that the appliance services. The AnyAgent deploys the appropriate files (.cer, .pem) within the defined directories and also performs and Inventory on the Items. + +This agent implements three job types – Inventory, Management Add, and Management Remove. Below are the steps necessary to configure this AnyAgent. It supports adding certificates with or without private keys. + + +**A10 vThunder Configuration** + +1. Read up on [A10 Networks ADC](https://a10networks.optrics.com/downloads/datasheets/Thunder-Application-Delivery-Controller-ADC.pdf) and how it works. +2. A user account is needed with the appropriate permissions on vThunder to manage certificates. + +**1. Create the New Certificate Store Type for the A10 vThunder Orchestrator** + +In Keyfactor Command create a new Certificate Store Type similar to the one below: + +#### STORE TYPE CONFIGURATION +SETTING TAB | CONFIG ELEMENT | DESCRIPTION +------|-----------|------------------ +Basic |Name |Descriptive name for the Store Type. A10 vThunder can be used. +Basic |Short Name |The short name that identifies the registered functionality of the orchestrator. Must be vThunderU +Basic |Custom Capability|Un checked +Basic |Job Types |Inventory, Add, and Remove are the supported job types. +Basic |Needs Server |Must be checked +Basic |Blueprint Allowed |checked +Basic |Requires Store Password |Determines if a store password is required when configuring an individual store. This must be unchecked. +Basic |Supports Entry Password |Determined if an individual entry within a store can have a password. This must be unchecked. +Advanced |Store Path Type| Determines how the user will enter the store path when setting up the cert store. Freeform +Advanced |Supports Custom Alias |Determines if an individual entry within a store can have a custom Alias. This must be Required +Advanced |Private Key Handling |Determines how the orchestrator deals with private keys. Optional +Advanced |PFX Password Style |Determines password style for the PFX Password. Default +Custom Fields|protocol|Name:protocol Display Name:Protocol Type:Multiple Choice (http,https) Default Value:https Required:True +Custom Fields|allowInvalidCert|Name:allowInvalidCert Display Name:Allow Invalid Cert Type:Bool Default Value:false Required:True +Entry Parameters|N/A| There are no Entry Parameters + +**Basic Settings:** + +![](Media/Images/CertStoreType-Basic.gif) + +**Advanced Settings:** + +![](Media/Images/CertStoreType-Advanced.gif) + +**Custom Fields:** + +![](Media/Images/CertStoreType-CustomFields.gif) + +**Entry Params:** + +![](Media/Images/CertStoreType-EntryParameters.gif) + +**2. Register the A10 vThunder Orchestrator with Keyfactor** +See Keyfactor InstallingKeyfactorOrchestrators.pdf Documentation. Get from your Keyfactor contact/representative. + +**3. Create a A10 vThunder Certificate Store within Keyfactor Command** +In Keyfactor Command create a new Certificate Store similar to the one below + +![](Media/Images/CertStore1.gif) +![](Media/Images/CertStore2.gif) + +#### STORE CONFIGURATION +CONFIG ELEMENT |DESCRIPTION +----------------|--------------- +Category |The type of certificate store to be configured. Select category based on the display name configured above "VThunder Universal". +Container |This is a logical grouping of like stores. This configuration is optional and does not impact the functionality of the store. +Client Machine |The url to the vThunder api. This file should the url and port of the vThunder api sample vThunder.test.com:1113. +Store Path |This will be "cert". This is not used but just hard code it as "cert". +Allow Invalid Cert|Only used for testing should be false in production. +Protocol| http is only used for testing should be https in production +Orchestrator |This is the orchestrator server registered with the appropriate capabilities to manage this certificate store type. +Inventory Schedule |The interval that the system will use to report on what certificates are currently in the store. +Use SSL |This should be checked. +User |This is the user name for the vThunder api to access the certficate management functionality. +Password |This is the password for the vThunder api to access the certficate management functionality. + +*** + +#### Usage + +**Adding New Certificate New Alias** + +![](Media/Images/NewCertNewAlias.gif) + +*** + +**Replace Cert With Same Alias** + +![](Media/Images/ReplaceCertSameAlias.gif) + +*** + +**Add Cert No Private Key** + +![](Media/Images/AddPubCert.gif) + +*** + +**Replace Cert No Private Key** + +![](Media/Images/PubCertReplace.gif) + +*** + +**Remove Cert No Private Key** + +![](Media/Images/RemovePubCert.gif) + +*** + +**Remove Cert and Private Key** + +![](Media/Images/RemoveCertAndKey.gif) + +*** + +**Certificate Inventory** + +![](Media/Images/CertificateInventory.gif) + +#### TEST CASES +Case Number|Case Name|Case Description|Overwrite Flag|Alias Name|Expected Results|Passed +------------|---------|----------------|--------------|----------|----------------|-------------- +1|Fresh Add With Alias|Will create new certificate and private key on the vThunder appliance|true|KeyAndCertBTest|The new KeyAndCertBTest certificate and private key will be created in the ADC/SSL Cerificates area on vThunder.|True +1a|Replace Alias with no overwrite flag|Should warn user that a cert cannot be replaced with the same name without overwrite flag|false|KeyAndCertBTest|Error Saying Overwrite Flag Needs To Be Used|True +1b|Replace Alias with overwrite flag|Will create new certificate and private key on the vThunder appliance|true|KeyAndCertBTest|Cert will be replaced because overwrite flag was used|True +2|Add Cert Without Private Key|This will create a cert with no private key on vThunder|false|NewCertNoPk|Only Cert will be added to vThunder with no private key|True +2a|Replace Cert Without Private Key|This will Replace a cert with no private key on vThunder|true|NewCertNoPk|Only Cert will be replaced on vThunder with no private key|True +2b|Replace Cert Without Private Key no overwrite flag|Should warn user that a cert cannot be replaced with the same name without overwrite flag|false|NewCertNoPk|Error Saying Overwrite Flag Needs To Be Used|True +3|Remove Certificate and Private Key|Certificate and Private Key Will Be Removed from A10|N/A|KeyAndCertBTest|Cert and Key will be removed from vThunder and Keyfactor Store|True +3a|Remove Certificate without Private Key|Certificate Will Be Removed from A10|N/A|KeyAndCertBTest|Cert will be removed from vThunder and Keyfactor Store|True +4|Inventory Certificates with Private Key|Inventory of Certificates with private keys will be pulled from vThunder up to 125 tested|N/A|N/A|125 Certs will be inventoried, more should be supported but there is no paging in the API so limits apply|True +4a|Inventory Certificates without Private Key|Inventory of Certificates without private keys will be pulled from vThunder up to 125 tested|N/A|N/A|125 Certs will be inventoried, more should be supported but there is no paging in the API so limits apply|True + +