diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..671250d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# See GitHub's documentation for more information on this file: +# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 66468fd..16f5091 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -1,42 +1,27 @@ -name: Starter Workflow -on: [workflow_dispatch, push, pull_request] +name: Keyfactor Bootstrap Workflow -jobs: - call-create-github-release-workflow: - uses: Keyfactor/actions/.github/workflows/github-release.yml@main - - get-manifest-properties: - runs-on: windows-latest - outputs: - update_catalog: ${{ steps.read-json.outputs.prop }} - steps: - - uses: actions/checkout@v3 - - name: Read json - id: read-json - shell: pwsh - run: | - $json = Get-Content integration-manifest.json | ConvertFrom-Json - echo "::set-output name=prop::$(echo $json.update_catalog)" +on: + workflow_dispatch: + pull_request: + types: [opened, closed, synchronize, edited, reopened] + push: + create: + branches: + - 'release-*.*' - call-dotnet-build-and-release-workflow: - needs: [call-create-github-release-workflow] - uses: Keyfactor/actions/.github/workflows/dotnet-build-and-release.yml@main +jobs: + call-starter-workflow: + uses: keyfactor/actions/.github/workflows/starter.yml@v4 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: a10vthunder-orchestrator\bin\Release\netcoreapp3.1 - 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 + command_token_url: ${{ vars.COMMAND_TOKEN_URL }} + command_hostname: ${{ vars.COMMAND_HOSTNAME }} + command_base_api_path: ${{ vars.COMMAND_API_PATH }} secrets: - token: ${{ secrets.APPROVE_README_PUSH }} - - call-update-catalog-workflow: - needs: get-manifest-properties - if: needs.get-manifest-properties.outputs.update_catalog == 'True' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') - uses: Keyfactor/actions/.github/workflows/update-catalog.yml@main - secrets: - token: ${{ secrets.SDK_SYNC_PAT }} + token: ${{ secrets.V2BUILDTOKEN}} + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} + scan_token: ${{ secrets.SAST_TOKEN }} + entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} + entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} + command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} + command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} \ No newline at end of file diff --git a/a10vthunder-orchestrator.sln b/A10vThunder.sln similarity index 54% rename from a10vthunder-orchestrator.sln rename to A10vThunder.sln index 7504fcb..32721dd 100644 --- a/a10vthunder-orchestrator.sln +++ b/A10vThunder.sln @@ -1,9 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31702.278 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35222.181 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "a10vthunder-orchestrator", "a10vthunder-orchestrator\a10vthunder-orchestrator.csproj", "{0E9426F8-B45E-4266-BB6C-7942D1B6B650}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "A10vThunder", "a10vthunder-orchestrator\A10vThunder.csproj", "{0E9426F8-B45E-4266-BB6C-7942D1B6B650}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "A10vThunderTestConsole", "A10vThunderTestConsole\A10vThunderTestConsole.csproj", "{EB627575-4C56-45D3-AC41-C9459D30D37B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {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 + {EB627575-4C56-45D3-AC41-C9459D30D37B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB627575-4C56-45D3-AC41-C9459D30D37B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB627575-4C56-45D3-AC41-C9459D30D37B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB627575-4C56-45D3-AC41-C9459D30D37B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/A10vThunderTestConsole/A10MgmtInventory.json b/A10vThunderTestConsole/A10MgmtInventory.json new file mode 100644 index 0000000..b00b49e --- /dev/null +++ b/A10vThunderTestConsole/A10MgmtInventory.json @@ -0,0 +1,260 @@ +{ + "LastInventory": [ + { + "Alias": "GeaugaRoof", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B8D46056C088892258A894EBCB599BC539A9724C" + ] + }, + { + "Alias": "NewCert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "1958A89E0CA8C9A54849D738709A4FE1ED870855" + ] + }, + { + "Alias": "brian", + "PrivateKeyEntry": false, + "Thumbprints": [ + "634FB01FFBACCBB9EC9E8DF29AE067F73A40A991" + ] + }, + { + "Alias": "hello", + "PrivateKeyEntry": false, + "Thumbprints": [ + "869F410795AC751EE2D8E6B391DABC408CA384F0" + ] + }, + { + "Alias": "evan", + "PrivateKeyEntry": false, + "Thumbprints": [ + "75D738EB5E2CB49AEBF12DCC899A92BD084FB475" + ] + }, + { + "Alias": "darrius", + "PrivateKeyEntry": false, + "Thumbprints": [ + "29C4E2C4C1C4036CAB0F23B78EEC17FAE158A8F1" + ] + }, + { + "Alias": "face", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B43991B7D02C9B9604D3E2DC37F161357CAD2EE8" + ] + }, + { + "Alias": "ac", + "PrivateKeyEntry": false, + "Thumbprints": [ + "C9DD4A1D8C203E0707B30C82DF6D814E098DCD70" + ] + }, + { + "Alias": "palodemocert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "C552053047ECA29524031745174E0800C1525282" + ] + }, + { + "Alias": "palocommitall", + "PrivateKeyEntry": false, + "Thumbprints": [ + "F53CB33F74A8EE262110E2C302C4051FC73504ED" + ] + }, + { + "Alias": "newpanoramacert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "D72A8BDF3EE7C1848FF05882CA71E1C12466E124" + ] + }, + { + "Alias": "tscommit", + "PrivateKeyEntry": false, + "Thumbprints": [ + "EABF46E628B18400BCB4B89ADCC34B340E8BEA1A" + ] + }, + { + "Alias": "trycommitnow", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B5DCFE076FB571CA22B36BC6205B9C7A9063EC52" + ] + }, + { + "Alias": "OGCommit", + "PrivateKeyEntry": false, + "Thumbprints": [ + "7765061EEC4E83FE7DF37C624774E89A486D1576" + ] + }, + { + "Alias": "committodevices2", + "PrivateKeyEntry": false, + "Thumbprints": [ + "6506124604691F8B68064EA095B1635C72A9A07A" + ] + }, + { + "Alias": "committodevices1", + "PrivateKeyEntry": false, + "Thumbprints": [ + "970D8EEB0F99D711322717B9CA5FDD2B93859BD7" + ] + }, + { + "Alias": "AnotherCommit", + "PrivateKeyEntry": false, + "Thumbprints": [ + "C156B89D1E0984140212DA28F26A0D313E3183C0" + ] + }, + { + "Alias": "sleepy1", + "PrivateKeyEntry": false, + "Thumbprints": [ + "8FADE71D3B92BF90BBC975B931A55E55D272F7F8" + ] + }, + { + "Alias": "Sleepy120", + "PrivateKeyEntry": false, + "Thumbprints": [ + "FC0510BEF565F43653D8EFDA7277A08E2D4EAFA5" + ] + }, + { + "Alias": "120try2", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B2C5FE62DD08B021BE9E45FF97F3A8E1D2550A81" + ] + }, + { + "Alias": "120Try3", + "PrivateKeyEntry": false, + "Thumbprints": [ + "8B9AB8305EB2C34C0E876FE58DEDC96B1106987C" + ] + }, + { + "Alias": "pfxEnrollTest", + "PrivateKeyEntry": false, + "Thumbprints": [ + "A668CD6908CF4373F7582103CFF204ACC64C8EB3" + ] + }, + { + "Alias": "BindingsTest2", + "PrivateKeyEntry": false, + "Thumbprints": [ + "C33F39D4DA97EF4FFB98464AAC6072A30C22A1B8" + ] + }, + { + "Alias": "BindingsTest3", + "PrivateKeyEntry": false, + "Thumbprints": [ + "FC14DEAB5F79EF137C8DECF2F0903F13C5DB2C75" + ] + }, + { + "Alias": "BindingsCert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "30724888B219D726FDA20CEC51C6FF2EAF995140" + ] + }, + { + "Alias": "BrianHill33", + "PrivateKeyEntry": false, + "Thumbprints": [ + "A9E0FF9319DC17820E0804D74CE6BE819C3CA06D" + ] + }, + { + "Alias": "PaloBindingsTest", + "PrivateKeyEntry": false, + "Thumbprints": [ + "48AB8F689A34C7D891C403CBDDD11710B347F4EE" + ] + }, + { + "Alias": "TestBindingsName", + "PrivateKeyEntry": false, + "Thumbprints": [ + "A1E76DDB960797EDBCFBD403AC6466720B8E4642" + ] + }, + { + "Alias": "BrianBinder", + "PrivateKeyEntry": false, + "Thumbprints": [ + "50CB0A34E63D25509B8CF6045F868DDD9ED6CF70" + ] + }, + { + "Alias": "BenderBinder", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B30E73266B6F3669DC8AA6859DFF5E64090D2495" + ] + }, + { + "Alias": "BryceAlexander", + "PrivateKeyEntry": false, + "Thumbprints": [ + "00D132EDEC0BA3CB9623FACAF9176C5E52B77A8C" + ] + }, + { + "Alias": "SpeakerCert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "5BD66F21A08CDC287A9BF2BAA538BF33D229FBAA" + ] + }, + { + "Alias": "CertAndBindingsToPA", + "PrivateKeyEntry": false, + "Thumbprints": [ + "72434177210E3D1C63A08E0C26C7A74F7AA4F057" + ] + }, + { + "Alias": "BindingsPlugTest", + "PrivateKeyEntry": false, + "Thumbprints": [ + "A3FD156359129C8F8667879C6360EC2DF38FFDBE" + ] + } + ], + "CertificateStoreDetails": { + "ClientMachine": "ClientMachineGoesHere", + "StorePath": "ScpPathGoesHere", + "StorePassword": null, + "Properties": "{\"ServerUsername\":\"UserNameGoesHere\",\"ServerPassword\":\"PasswordGoesHere\",\"ServerUseSsl\":\"true\",\"ServerUseSsl\":\"true\",\"OrchToScpServerIp\":\"OrchToScpServerIpGoesHere\",\"ScpPort\":\"ScpPortGoesHere\",\"ScpUserName\":\"ScpUserGoesHere\",\"ScpPassword\":\"ScpPwdGoesHere\",\"A10ToScpServerIp\":\"A10ToScpServerIpGoesHere\",\"allowInvalidCert\":\"true\"}", + "Type": 106 + }, + "JobCancelled": false, + "ServerError": null, + "JobHistoryId": 22881, + "RequestStatus": 1, + "ServerUsername": "UserNameGoesHere", + "ServerPassword": "PasswordGoesHere", + "UseSSL": true, + "JobProperties": null, + "JobTypeId": "00000000-0000-0000-0000-000000000000", + "JobId": "c7785480-8b15-4e12-b55d-3f73735cad6b", + "Capability": "CertStores.ThunderMgmt.Inventory" +} \ No newline at end of file diff --git a/A10vThunderTestConsole/A10MgmtManagement.json b/A10vThunderTestConsole/A10MgmtManagement.json new file mode 100644 index 0000000..493bd42 --- /dev/null +++ b/A10vThunderTestConsole/A10MgmtManagement.json @@ -0,0 +1,30 @@ +{ + "LastInventory": [], + "CertificateStoreDetails": { + "ClientMachine": "ClientMachineGoesHere", + "StorePath": "ScpPathGoesHere", + "StorePassword": null, + "Properties": "{\"ServerUsername\":\"UserNameGoesHere\",\"ServerPassword\":\"PasswordGoesHere\",\"ServerUseSsl\":\"true\",\"ServerUseSsl\":\"true\",\"OrchToScpServerIp\":\"OrchToScpServerIpGoesHere\",\"ScpPort\":\"ScpPortGoesHere\",\"ScpUserName\":\"ScpUserGoesHere\",\"ScpPassword\":\"ScpPwdGoesHere\",\"A10ToScpServerIp\":\"A10ToScpServerIpGoesHere\",\"allowInvalidCert\":\"true\"}", + "Type": 106 + }, + "OperationType": 2, + "Overwrite": false, + "JobCertificate": { + "Thumbprint": null, + "Contents": "CertificateContentGoesHere", + "Alias": "AliasGoesHere", + "PrivateKeyPassword": "sldfklsdfsldjfk" + }, + "JobCancelled": false, + "ServerError": null, + "JobHistoryId": 22907, + "RequestStatus": 1, + "ServerUsername": "UserNameGoesHere", + "ServerPassword": "PasswordGoesHere", + "UseSSL": true, + "JobProperties": { + }, + "JobTypeId": "00000000-0000-0000-0000-000000000000", + "JobId": "6808e1a2-04bb-4008-89fc-649662c0cd2b", + "Capability": "CertStores.ThunderMgmt.Management" +} \ No newline at end of file diff --git a/A10vThunderTestConsole/A10MgmtManagementRemove.json b/A10vThunderTestConsole/A10MgmtManagementRemove.json new file mode 100644 index 0000000..cfb326e --- /dev/null +++ b/A10vThunderTestConsole/A10MgmtManagementRemove.json @@ -0,0 +1,30 @@ +{ + "LastInventory": [], + "CertificateStoreDetails": { + "ClientMachine": "ClientMachineGoesHere", + "StorePath": "ScpPathGoesHere", + "StorePassword": null, + "Properties": "{\"ServerUsername\":\"UserNameGoesHere\",\"ServerPassword\":\"PasswordGoesHere\",\"ServerUseSsl\":\"true\",\"ServerUseSsl\":\"true\",\"OrchToScpServerIp\":\"OrchToScpServerIpGoesHere\",\"ScpPort\":\"ScpPortGoesHere\",\"ScpUserName\":\"ScpUserGoesHere\",\"ScpPassword\":\"ScpPwdGoesHere\",\"A10ToScpServerIp\":\"A10ToScpServerIpGoesHere\",\"allowInvalidCert\":\"true\"}", + "Type": 106 + }, + "OperationType": 3, + "Overwrite": false, + "JobCertificate": { + "Thumbprint": null, + "Contents": "", + "Alias": "AliasGoesHere", + "PrivateKeyPassword": null + }, + "JobCancelled": false, + "ServerError": null, + "JobHistoryId": 22908, + "RequestStatus": 1, + "ServerUsername": "UserNameGoesHere", + "ServerPassword": "PasswordGoesHere", + "UseSSL": true, + "JobProperties": { + }, + "JobTypeId": "00000000-0000-0000-0000-000000000000", + "JobId": "ba6248e2-eb3f-4403-9974-8df0e9f15f98", + "Capability": "CertStores.ThunderMgmt.Management" +} \ No newline at end of file diff --git a/A10vThunderTestConsole/A10SslInventory.json b/A10vThunderTestConsole/A10SslInventory.json new file mode 100644 index 0000000..4a29d28 --- /dev/null +++ b/A10vThunderTestConsole/A10SslInventory.json @@ -0,0 +1,260 @@ +{ + "LastInventory": [ + { + "Alias": "GeaugaRoof", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B8D46056C088892258A894EBCB599BC539A9724C" + ] + }, + { + "Alias": "NewCert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "1958A89E0CA8C9A54849D738709A4FE1ED870855" + ] + }, + { + "Alias": "brian", + "PrivateKeyEntry": false, + "Thumbprints": [ + "634FB01FFBACCBB9EC9E8DF29AE067F73A40A991" + ] + }, + { + "Alias": "hello", + "PrivateKeyEntry": false, + "Thumbprints": [ + "869F410795AC751EE2D8E6B391DABC408CA384F0" + ] + }, + { + "Alias": "evan", + "PrivateKeyEntry": false, + "Thumbprints": [ + "75D738EB5E2CB49AEBF12DCC899A92BD084FB475" + ] + }, + { + "Alias": "darrius", + "PrivateKeyEntry": false, + "Thumbprints": [ + "29C4E2C4C1C4036CAB0F23B78EEC17FAE158A8F1" + ] + }, + { + "Alias": "face", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B43991B7D02C9B9604D3E2DC37F161357CAD2EE8" + ] + }, + { + "Alias": "ac", + "PrivateKeyEntry": false, + "Thumbprints": [ + "C9DD4A1D8C203E0707B30C82DF6D814E098DCD70" + ] + }, + { + "Alias": "palodemocert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "C552053047ECA29524031745174E0800C1525282" + ] + }, + { + "Alias": "palocommitall", + "PrivateKeyEntry": false, + "Thumbprints": [ + "F53CB33F74A8EE262110E2C302C4051FC73504ED" + ] + }, + { + "Alias": "newpanoramacert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "D72A8BDF3EE7C1848FF05882CA71E1C12466E124" + ] + }, + { + "Alias": "tscommit", + "PrivateKeyEntry": false, + "Thumbprints": [ + "EABF46E628B18400BCB4B89ADCC34B340E8BEA1A" + ] + }, + { + "Alias": "trycommitnow", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B5DCFE076FB571CA22B36BC6205B9C7A9063EC52" + ] + }, + { + "Alias": "OGCommit", + "PrivateKeyEntry": false, + "Thumbprints": [ + "7765061EEC4E83FE7DF37C624774E89A486D1576" + ] + }, + { + "Alias": "committodevices2", + "PrivateKeyEntry": false, + "Thumbprints": [ + "6506124604691F8B68064EA095B1635C72A9A07A" + ] + }, + { + "Alias": "committodevices1", + "PrivateKeyEntry": false, + "Thumbprints": [ + "970D8EEB0F99D711322717B9CA5FDD2B93859BD7" + ] + }, + { + "Alias": "AnotherCommit", + "PrivateKeyEntry": false, + "Thumbprints": [ + "C156B89D1E0984140212DA28F26A0D313E3183C0" + ] + }, + { + "Alias": "sleepy1", + "PrivateKeyEntry": false, + "Thumbprints": [ + "8FADE71D3B92BF90BBC975B931A55E55D272F7F8" + ] + }, + { + "Alias": "Sleepy120", + "PrivateKeyEntry": false, + "Thumbprints": [ + "FC0510BEF565F43653D8EFDA7277A08E2D4EAFA5" + ] + }, + { + "Alias": "120try2", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B2C5FE62DD08B021BE9E45FF97F3A8E1D2550A81" + ] + }, + { + "Alias": "120Try3", + "PrivateKeyEntry": false, + "Thumbprints": [ + "8B9AB8305EB2C34C0E876FE58DEDC96B1106987C" + ] + }, + { + "Alias": "pfxEnrollTest", + "PrivateKeyEntry": false, + "Thumbprints": [ + "A668CD6908CF4373F7582103CFF204ACC64C8EB3" + ] + }, + { + "Alias": "BindingsTest2", + "PrivateKeyEntry": false, + "Thumbprints": [ + "C33F39D4DA97EF4FFB98464AAC6072A30C22A1B8" + ] + }, + { + "Alias": "BindingsTest3", + "PrivateKeyEntry": false, + "Thumbprints": [ + "FC14DEAB5F79EF137C8DECF2F0903F13C5DB2C75" + ] + }, + { + "Alias": "BindingsCert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "30724888B219D726FDA20CEC51C6FF2EAF995140" + ] + }, + { + "Alias": "BrianHill33", + "PrivateKeyEntry": false, + "Thumbprints": [ + "A9E0FF9319DC17820E0804D74CE6BE819C3CA06D" + ] + }, + { + "Alias": "PaloBindingsTest", + "PrivateKeyEntry": false, + "Thumbprints": [ + "48AB8F689A34C7D891C403CBDDD11710B347F4EE" + ] + }, + { + "Alias": "TestBindingsName", + "PrivateKeyEntry": false, + "Thumbprints": [ + "A1E76DDB960797EDBCFBD403AC6466720B8E4642" + ] + }, + { + "Alias": "BrianBinder", + "PrivateKeyEntry": false, + "Thumbprints": [ + "50CB0A34E63D25509B8CF6045F868DDD9ED6CF70" + ] + }, + { + "Alias": "BenderBinder", + "PrivateKeyEntry": false, + "Thumbprints": [ + "B30E73266B6F3669DC8AA6859DFF5E64090D2495" + ] + }, + { + "Alias": "BryceAlexander", + "PrivateKeyEntry": false, + "Thumbprints": [ + "00D132EDEC0BA3CB9623FACAF9176C5E52B77A8C" + ] + }, + { + "Alias": "SpeakerCert", + "PrivateKeyEntry": false, + "Thumbprints": [ + "5BD66F21A08CDC287A9BF2BAA538BF33D229FBAA" + ] + }, + { + "Alias": "CertAndBindingsToPA", + "PrivateKeyEntry": false, + "Thumbprints": [ + "72434177210E3D1C63A08E0C26C7A74F7AA4F057" + ] + }, + { + "Alias": "BindingsPlugTest", + "PrivateKeyEntry": false, + "Thumbprints": [ + "A3FD156359129C8F8667879C6360EC2DF38FFDBE" + ] + } + ], + "CertificateStoreDetails": { + "ClientMachine": "ClientMachineGoesHere", + "StorePath": "PartitionNameGoesHere", + "StorePassword": "", + "Properties": "{\"ServerUsername\":\"UserNameGoesHere\",\"ServerPassword\":\"PasswordGoesHere\",\"ServerUseSsl\":\"true\",\"allowInvalidCert\":\"true\"}", + "Type": 105 + }, + "JobCancelled": false, + "ServerError": null, + "JobHistoryId": 22881, + "RequestStatus": 1, + "ServerUsername": "UserNameGoesHere", + "ServerPassword": "PasswordGoesHere", + "UseSSL": true, + "JobProperties": null, + "JobTypeId": "00000000-0000-0000-0000-000000000000", + "JobId": "c7785480-8b15-4e12-b55d-3f73735cad6b", + "Capability": "CertStores.ThunderSsl.Inventory" +} \ No newline at end of file diff --git a/A10vThunderTestConsole/A10SslManagement.json b/A10vThunderTestConsole/A10SslManagement.json new file mode 100644 index 0000000..f794446 --- /dev/null +++ b/A10vThunderTestConsole/A10SslManagement.json @@ -0,0 +1,30 @@ +{ + "LastInventory": [], + "CertificateStoreDetails": { + "ClientMachine": "ClientMachineGoesHere", + "StorePath": "PartitionNameGoesHere", + "StorePassword": null, + "Properties": "{\"ServerUsername\":\"UserNameGoesHere\",\"ServerPassword\":\"PasswordGoesHere\",\"ServerUseSsl\":\"true\",\"allowInvalidCert\":\"true\"}", + "Type": 105 + }, + "OperationType": 2, + "Overwrite": false, + "JobCertificate": { + "Thumbprint": null, + "Contents": "CertificateContentGoesHere", + "Alias": "AliasGoesHere", + "PrivateKeyPassword": "sldfklsdfsldjfk" + }, + "JobCancelled": false, + "ServerError": null, + "JobHistoryId": 22907, + "RequestStatus": 1, + "ServerUsername": "UserNameGoesHere", + "ServerPassword": "PasswordGoesHere", + "UseSSL": true, + "JobProperties": { + }, + "JobTypeId": "00000000-0000-0000-0000-000000000000", + "JobId": "6808e1a2-04bb-4008-89fc-649662c0cd2b", + "Capability": "CertStores.ThunderSsl.Management" +} \ No newline at end of file diff --git a/A10vThunderTestConsole/A10SsllManagementRemove.json b/A10vThunderTestConsole/A10SsllManagementRemove.json new file mode 100644 index 0000000..4a06dc5 --- /dev/null +++ b/A10vThunderTestConsole/A10SsllManagementRemove.json @@ -0,0 +1,30 @@ +{ + "LastInventory": [], + "CertificateStoreDetails": { + "ClientMachine": "ClientMachineGoesHere", + "StorePath": "PartitionNameGoesHere", + "StorePassword": null, + "Properties": "{\"ServerUsername\":\"UserNameGoesHere\",\"ServerPassword\":\"PasswordGoesHere\",\"ServerUseSsl\":\"true\",\"ServerUseSsl\":\"true\",\"allowInvalidCert\":\"true\"}", + "Type": 105 + }, + "OperationType": 3, + "Overwrite": false, + "JobCertificate": { + "Thumbprint": null, + "Contents": "", + "Alias": "AliasGoesHere", + "PrivateKeyPassword": null + }, + "JobCancelled": false, + "ServerError": null, + "JobHistoryId": 22908, + "RequestStatus": 1, + "ServerUsername": "UserNameGoesHere", + "ServerPassword": "PasswordGoesHere", + "UseSSL": true, + "JobProperties": { + }, + "JobTypeId": "00000000-0000-0000-0000-000000000000", + "JobId": "ba6248e2-eb3f-4403-9974-8df0e9f15f98", + "Capability": "CertStores.ThunderSsl.Management" +} \ No newline at end of file diff --git a/A10vThunderTestConsole/A10vThunderTestConsole.csproj b/A10vThunderTestConsole/A10vThunderTestConsole.csproj new file mode 100644 index 0000000..b9c6f8c --- /dev/null +++ b/A10vThunderTestConsole/A10vThunderTestConsole.csproj @@ -0,0 +1,56 @@ + + + + Exe + true + net6.0;net8.0 + true + disable + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/A10vThunderTestConsole/KeyfactorClient.cs b/A10vThunderTestConsole/KeyfactorClient.cs new file mode 100644 index 0000000..0532246 --- /dev/null +++ b/A10vThunderTestConsole/KeyfactorClient.cs @@ -0,0 +1,56 @@ +// 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; +using System.Collections.Generic; +using System.Threading.Tasks; +using RestSharp; + + +namespace A10vThunderTestConsole +{ + public class KeyfactorClient + { + public async Task EnrollCertificate(string commonName) + { + var options = new RestClientOptions("https://bhillkf10.kfdelivery.com"); + var client = new RestClient(options); + var request = new RestRequest("/KeyfactorAPI/Enrollment/PFX", Method.Post); + request.AddHeader("X-Keyfactor-Requested-With", "APIClient"); + request.AddHeader("x-certificateformat", "PFX"); + request.AddHeader("Authorization", "Basic sdf="); + request.AddHeader("Content-Type", "application/json"); + var enrollRequest = new KeyfactorEnrollmentRequest + { + Password = "sldfklsdfsldjfk", + PopulateMissingValuesFromAD = false, + Subject = $"CN={commonName},C=US", + IncludeChain = true, + RenewalCertificateId = 0, + CertificateAuthority = "DC-CA.Command.local\\CommandCA1", + Timestamp = DateTime.Now, + Template = "2YearTestWebServer" + }; + SANs sans = new SANs(); + List dnsList = new List { $"{commonName}" }; + sans.DNS = dnsList; + enrollRequest.SANs = sans; + request.AddBody(enrollRequest); + var response = await client.ExecutePostAsync(request); + return response.Data; + + } + + } +} diff --git a/A10vThunderTestConsole/KeyfactorEnrollmentRequest.cs b/A10vThunderTestConsole/KeyfactorEnrollmentRequest.cs new file mode 100644 index 0000000..c691a01 --- /dev/null +++ b/A10vThunderTestConsole/KeyfactorEnrollmentRequest.cs @@ -0,0 +1,39 @@ +// 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; +using System.Collections.Generic; +using System.Text; + +namespace A10vThunderTestConsole +{ + public class KeyfactorEnrollmentRequest + { + public string CustomFriendlyName { get; set; } + public string Password { get; set; } + public bool PopulateMissingValuesFromAD { get; set; } + public string Subject { get; set; } + public bool IncludeChain { get; set; } + public int RenewalCertificateId { get; set; } + public string CertificateAuthority { get; set; } + public DateTime Timestamp { get; set; } + public string Template { get; set; } + public SANs SANs { get; set; } + } + + public class SANs + { + public List DNS { get; set; } + } +} diff --git a/A10vThunderTestConsole/KeyfactorEnrollmentResult.cs b/A10vThunderTestConsole/KeyfactorEnrollmentResult.cs new file mode 100644 index 0000000..c8f13df --- /dev/null +++ b/A10vThunderTestConsole/KeyfactorEnrollmentResult.cs @@ -0,0 +1,51 @@ +// 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; +using System.Collections.Generic; +using System.Text; + +namespace A10vThunderTestConsole +{ + // Root myDeserializedClass = JsonConvert.DeserializeObject(myJsonResponse); + public class CertificateInformation + { + public string SerialNumber { get; set; } + public string IssuerDN { get; set; } + public string Thumbprint { get; set; } + public int KeyfactorId { get; set; } + public string Pkcs12Blob { get; set; } + public object Password { get; set; } + public string WorkflowInstanceId { get; set; } + public int WorkflowReferenceId { get; set; } + public List StoreIdsInvalidForRenewal { get; set; } + public int KeyfactorRequestId { get; set; } + public string RequestDisposition { get; set; } + public string DispositionMessage { get; set; } + public object EnrollmentContext { get; set; } + } + + public class Metadata + { + public string OID { get; set; } + } + + public class KeyfactorEnrollmentResult + { + public CertificateInformation CertificateInformation { get; set; } + public Metadata Metadata { get; set; } + } + + +} diff --git a/A10vThunderTestConsole/Program.cs b/A10vThunderTestConsole/Program.cs new file mode 100644 index 0000000..5148339 --- /dev/null +++ b/A10vThunderTestConsole/Program.cs @@ -0,0 +1,304 @@ +// 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; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Keyfactor.Orchestrators.Extensions; +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace A10vThunderTestConsole +{ + internal class Program + { + public static string UserName { get; set; } + public static string Password { get; set; } + public static string CaseName { get; set; } + public static string CertAlias { get; set; } + public static string ClientMachine { get; set; } + public static string StorePath { get; set; } + public static string Overwrite { get; set; } + public static string ManagementType { get; set; } + public static string CertificateContent { get; set; } + public static string OrchToScpServerIp { get; set; } + public static string A10ToScpServerIp { get; set; } + public static string ScpPort { get; set; } + public static string ScpUserName { get; set; } + public static string ScpPassword { get; set; } + public static string StoreType { get; set; } + + + private static async Task Main(string[] args) + { + Thread.Sleep(20000); + var arguments = ParseArgs(args); + + CaseName = GetValue(arguments, "-casename", "Enter The Case Name Inventory or Management"); + UserName = GetValue(arguments, "-user", "Enter User Name"); + Password = GetValue(arguments, "-password", "Enter The Password"); + StorePath = GetValue(arguments, "-storepath", "Enter Store Path"); + StoreType = GetValue(arguments, "-storetype", "Enter Store Type"); + if (StoreType.ToLower() == "mgmt") + { + OrchToScpServerIp = GetValue(arguments, "-orchtoscpserverip", "Enter Orch To Scp Server Ip"); + A10ToScpServerIp = GetValue(arguments, "-a10toscpserverip", "Enter A10 to Scp Server Ip"); + ScpPort = GetValue(arguments, "-scpport", "Enter Scp Port"); + ScpUserName = GetValue(arguments, "-scpusername", "Enter Scp User Name"); + ScpPassword = GetValue(arguments, "-scppassword", "Enter Scp Password"); + } + ClientMachine = GetValue(arguments, "-clientmachine", "Enter Client Machine"); + + + Console.WriteLine("Running"); + + switch (CaseName?.ToUpper()) + { + case "INVENTORY": + RunInventory(arguments); + break; + + case "MANAGEMENT": + ManagementType = GetValue(arguments, "-managementtype", "Select Management Type Add or Remove"); + if (ManagementType?.ToUpper() == "ADD") + RunManagementAdd(arguments); + else if (ManagementType?.ToUpper() == "REMOVE") + RunManagementRemove(arguments); + break; + } + } + + private static Dictionary ParseArgs(string[] args) + { + var result = new Dictionary(); + foreach (var arg in args) + { + var parts = arg.Split('=', 2); + if (parts.Length == 2) + result[parts[0].ToLower()] = parts[1]; + } + return result; + } + + private static string GetValue(Dictionary args, string key, string prompt) + { + return args.TryGetValue(key.ToLower(), out var value) ? value : Prompt(prompt); + } + + private static string Prompt(string message) + { + Console.WriteLine(message); + return Console.ReadLine(); + } + + private static void RunInventory(Dictionary args) + { + StoreType = GetValue(args, "-storetype", "Enter Cert Alias"); + + Console.WriteLine("Running Inventory"); + var config = GetInventoryJobConfiguration(StoreType); + Console.WriteLine("Got Inventory Config"); + + var secretResolver = MockSecrets(config.ServerUsername, config.ServerPassword); + + JobResult result; + + if (StoreType.ToLower() == "ssl") + { + var inv = new Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderSsl.Inventory(secretResolver); + result = inv.ProcessJob(config, GetItems); + } + else + { + var inv = new Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderMgmt.Inventory(secretResolver); + result = inv.ProcessJob(config, GetItems); + } + + Console.WriteLine("Inventory Response:"); + Console.WriteLine(JsonConvert.SerializeObject(result)); + Console.ReadLine(); + } + + private static void RunManagementAdd(Dictionary args) + { + CertAlias = GetValue(args, "-certalias", "Enter Cert Alias"); + Overwrite = GetValue(args, "-overwrite", "Overwrite (True or False)?"); + StoreType = GetValue(args, "-storetype", "Enter Store Type"); + + Console.WriteLine("Generating Cert from KF API..."); + var client = new KeyfactorClient(); + CertificateContent = client.EnrollCertificate($"www.{CertAlias}.com").Result.CertificateInformation.Pkcs12Blob; + + var config = GetManagementJobConfiguration(StoreType); + var secretResolver = MockSecrets(config.ServerUsername, config.ServerPassword); + + JobResult result; + + if (StoreType.ToLower() == "ssl") + { + var mgmt = new Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderSsl.Management(secretResolver); + result = mgmt.ProcessJob(config); + } + else + { + var mgmt = new Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderMgmt.Management(secretResolver); + result = mgmt.ProcessJob(config); + } + Console.WriteLine(JsonConvert.SerializeObject(result)); + Console.ReadLine(); + } + + private static void RunManagementRemove(Dictionary args) + { + CertAlias = GetValue(args, "-certalias", "Enter Cert Alias"); + StoreType = GetValue(args, "-storetype", "Enter Store Type"); + + + var config = GetRemoveJobConfiguration(StoreType); + var secretResolver = MockSecrets(config.ServerUsername, config.ServerPassword); + JobResult result; + + if (StoreType.ToLower() == "ssl") + { + var mgmt = new Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderSsl.Management(secretResolver); + result = mgmt.ProcessJob(config); + } + else + { + var mgmt = new Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderMgmt.Management(secretResolver); + result = mgmt.ProcessJob(config); + } + Thread.Sleep(5000); + Console.WriteLine(JsonConvert.SerializeObject(result)); + Console.ReadLine(); + } + + private static IPAMSecretResolver MockSecrets(string username, string password) + { + var mock = new Mock(); + mock.Setup(m => m.Resolve(It.Is(s => s == username))).Returns(username); + mock.Setup(m => m.Resolve(It.Is(s => s == password))).Returns(password); + return mock.Object; + } + + public static bool GetItems(IEnumerable items) => true; + + private static InventoryJobConfiguration GetInventoryJobConfiguration(string storeType) + { + string fileContent = string.Empty; + + if (storeType.ToLower() == "ssl") + { + fileContent = File.ReadAllText($"A10SslInventory.json") + .Replace("UserNameGoesHere", UserName) + .Replace("PasswordGoesHere", Password) + .Replace("ClientMachineGoesHere", ClientMachine) + .Replace("PartitionNameGoesHere", StorePath); + } + else + { + fileContent = File.ReadAllText($"A10MgmtInventory.json") + .Replace("UserNameGoesHere", UserName) + .Replace("PasswordGoesHere", Password) + .Replace("ClientMachineGoesHere", ClientMachine) + .Replace("ScpPathGoesHere", StorePath) + .Replace("OrchToScpServerIpGoesHere", OrchToScpServerIp) + .Replace("A10ToScpServerIpGoesHere", A10ToScpServerIp) + .Replace("ScpPortGoesHere",ScpPort) + .Replace("ScpUserGoesHere", ScpUserName) + .Replace("ScpPwdGoesHere",ScpPassword); + } + + + + return JsonConvert.DeserializeObject(JObject.Parse(fileContent).ToString()); + } + + private static ManagementJobConfiguration GetManagementJobConfiguration(string storeType) + { + var overwriteValue = Overwrite?.ToUpper() == "TRUE" ? "true" : "false"; + + string fileContent; + if (storeType.ToLower() == "ssl") + { + fileContent = File.ReadAllText($"A10SslManagement.json") + .Replace("UserNameGoesHere", UserName) + .Replace("PasswordGoesHere", Password) + .Replace("PartitionNameGoesHere", StorePath) + .Replace("AliasGoesHere", CertAlias) + .Replace("ClientMachineGoesHere", ClientMachine) + .Replace("\"Overwrite\": false", $"\"Overwrite\": {overwriteValue}") + .Replace("CertificateContentGoesHere", CertificateContent); + } + else + { + fileContent = File.ReadAllText($"A10MgmtManagement.json") + .Replace("UserNameGoesHere", UserName) + .Replace("PasswordGoesHere", Password) + .Replace("PartitionNameGoesHere", StorePath) + .Replace("AliasGoesHere", CertAlias) + .Replace("ClientMachineGoesHere", ClientMachine) + .Replace("\"Overwrite\": false", $"\"Overwrite\": {overwriteValue}") + .Replace("ScpPathGoesHere", StorePath) + .Replace("OrchToScpServerIpGoesHere", OrchToScpServerIp) + .Replace("A10ToScpServerIpGoesHere", A10ToScpServerIp) + .Replace("ScpPortGoesHere", ScpPort) + .Replace("ScpUserGoesHere", ScpUserName) + .Replace("ScpPwdGoesHere", ScpPassword) + .Replace("CertificateContentGoesHere", CertificateContent); + } + + return JsonConvert.DeserializeObject(JObject.Parse(fileContent).ToString()); + } + + private static ManagementJobConfiguration GetRemoveJobConfiguration(string storeType) + { + + string fileContent = string.Empty; + + if (storeType.ToLower() == "ssl") + { + fileContent = File.ReadAllText($"A10SsllManagementRemove.json") + .Replace("UserNameGoesHere", UserName) + .Replace("PasswordGoesHere", Password) + .Replace("PartitionNameGoesHere", StorePath) + .Replace("AliasGoesHere", CertAlias) + .Replace("ClientMachineGoesHere", ClientMachine); + } + else + { + fileContent = File.ReadAllText($"A10MgmtManagementRemove.json") + .Replace("UserNameGoesHere", UserName) + .Replace("PasswordGoesHere", Password) + .Replace("PartitionNameGoesHere", StorePath) + .Replace("AliasGoesHere", CertAlias) + .Replace("ScpPathGoesHere", StorePath) + .Replace("OrchToScpServerIpGoesHere", OrchToScpServerIp) + .Replace("A10ToScpServerIpGoesHere", A10ToScpServerIp) + .Replace("ScpPortGoesHere", ScpPort) + .Replace("ScpUserGoesHere", ScpUserName) + .Replace("ScpPwdGoesHere", ScpPassword) + .Replace("ClientMachineGoesHere", ClientMachine); + } + + + + return JsonConvert.DeserializeObject(fileContent); + } + } +} diff --git a/A10vThunderTestConsole/RunTest.bat b/A10vThunderTestConsole/RunTest.bat new file mode 100644 index 0000000..64e0616 --- /dev/null +++ b/A10vThunderTestConsole/RunTest.bat @@ -0,0 +1,271 @@ +@echo off + +cd C:\Users\bhill\source\repos\a10vthunder-orchestrator\A10vThunderTestConsole\bin\Debug\net8.0 +set ClientMachine= +set A10ApiUser=admin +set A10ApiPassword= +set ScpUserName=ec2-user +set ScpPassword= +set ScpPort=22 +set OrchToScpServerIp= +set A10ToScpServerIp= + +set clientmachine=%ClientMachine% +set password=%A10ApiPassword% +set user=%A10ApiUser% +set storepath=shared + + + +echo *********************************** +echo Starting Ssl StoreType Test Cases +echo *********************************** + + +echo *********************************** +echo Starting Management Test Cases +echo *********************************** +set casename=Management + + +set cert=%random% +set casename=Management +set mgt=add +set overwrite=false + + +echo ************************************************************************************************************************ +echo TC1 %mgt%. Should do the %mgt% and add anything in the chain +echo ************************************************************************************************************************ +echo overwrite: %overwrite% +echo cert name: %cert% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + +set mgt=remove +set overwrite=false + +echo: +echo ******************************************************************************************************* +echo TC2 %mgt% unbound Cert. Should %mgt% the cert since there are no dependencies +echo ******************************************************************************************************* +echo overwrite: %overwrite% +echo cert name: %cert% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + +set mgt=add +set overwrite=false +set storepath=keyfactor2 + +echo: +echo ******************************************************************************************************* +echo TC3 %mgt% unbound Cert to New Different Partition. Should %mgt% the cert to Partition +echo ******************************************************************************************************* +echo overwrite: %overwrite% +echo cert name: %cert% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + +set mgt=remove + +echo: +echo ******************************************************************************************************* +echo TC4 %mgt% unbound Cert in Different Partition. Should %mgt% the cert from Partition +echo ******************************************************************************************************* +echo overwrite: %overwrite% +echo cert name: %cert% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + +set mgt=add +set overwrite=false +set storepath=shared +set cert=%random% + +echo: +echo ******************************************************************************************************* +echo TC5 Setup Unbound Renew Scenario Unbound Certificate +echo ******************************************************************************************************* +echo overwrite: %overwrite% +echo cert name: %cert% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + +set overwrite=false +set storepath=shared + +echo: +echo ******************************************************************************************************* +echo TC5a Unbound Renew certificate with no overwrite, should warn user that overwrite flag is required +echo ******************************************************************************************************* +echo overwrite: %overwrite% +echo cert name: %cert% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + +set mgt=add +set storepath=shared +set overwrite=true + +echo: +echo ******************************************************************************************************* +echo TC5b Unbound Renew certificate with overwrite, should overwrite since flag is used +echo ******************************************************************************************************* +echo overwrite: %overwrite% +echo cert name: %cert% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + + + +set mgt=add +set storepath=shared +set overwrite=true + +echo: +echo ******************************************************************************************************* +echo TC5c Bound Renew certificate with overwrite, should re-name cert and rebind it to all locations +echo ******************************************************************************************************* +echo overwrite: %overwrite% +set /p cert=Please enter bound cert name: +echo cert name: %cert% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + +set mgt=add +set storepath=keyfactor2 +set overwrite=true + +echo: +echo ****************************************************************************************************************** +echo TC6 Bound Renew Different Partition certificate with overwrite, should re-name cert and rebind it to all locations +echo ****************************************************************************************************************** +echo overwrite: %overwrite% +set /p cert=Please enter bound cert name: +echo cert name: %cert% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + + +set mgt=remove +set storepath=keyfactor2 +set overwrite=true + +echo: +echo ****************************************************************************************************************** +echo TC7 Remove Bound certificate - This should not be allowed and should error out +echo ****************************************************************************************************************** +echo overwrite: %overwrite% +set /p cert=Please enter bound cert name: +echo cert name: %cert% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + + +set casename=Inventory +set storepath=keyfactor2 + + +echo: +echo ****************************************************************************************************************** +echo TC8 Inventory from a Partition +echo ****************************************************************************************************************** +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + +:TC8 + +set casename=Inventory +set storepath=shared + + +echo: +echo ****************************************************************************************************************** +echo TC9 Inventory from shared location +echo ****************************************************************************************************************** +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=ssl + +echo *********************************** +echo Starting mgmt StoreType Test Cases +echo *********************************** + +:TC10 + +echo *********************************** +echo Starting Management Test Cases +echo *********************************** +set casename=Management + +set mgt=add +set storepath=/home/ec2-user +set overwrite=true +set cert=%random% + +echo: +echo ****************************************************************************************************************** +echo TC10 Add New Certificate to Location and Bind to Management Port +echo ****************************************************************************************************************** +echo overwrite: %overwrite% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=mgmt -orchtoscpserverip=%OrchToScpServerIp% -a10toscpserverip=%A10ToScpServerIp% -scpusername=%ScpUserName% -scppassword=%ScpPassword% -scpport=%ScpPort% + + +echo: +echo ****************************************************************************************************************** +echo TC11 Renew certificate bound to Management Port should rep and bind to mgmt port +echo ****************************************************************************************************************** +echo overwrite: %overwrite% +echo storepath: %storepath% + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=mgmt -orchtoscpserverip=%OrchToScpServerIp% -a10toscpserverip=%A10ToScpServerIp% -scpusername=%ScpUserName% -scppassword=%ScpPassword% -scpport=%ScpPort% + + +echo: +echo ****************************************************************************************************************** +echo TC12 Renew/Repl certificate bound to Management Port with out overwrite, should fail and warn user +echo ****************************************************************************************************************** +set overwrite=false +echo overwrite: %overwrite% +echo storepath: %storepath% + + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=mgmt -orchtoscpserverip=%OrchToScpServerIp% -a10toscpserverip=%A10ToScpServerIp% -scpusername=%ScpUserName% -scppassword=%ScpPassword% -scpport=%ScpPort% + + +echo: +echo ****************************************************************************************************************** +echo TC13 Remove certificate bound to Management Port should remove the cert and leave the binding in place +echo ****************************************************************************************************************** +echo overwrite: %overwrite% +echo storepath: %storepath% +set mgt=remove + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=mgmt -orchtoscpserverip=%OrchToScpServerIp% -a10toscpserverip=%A10ToScpServerIp% -scpusername=%ScpUserName% -scppassword=%ScpPassword% -scpport=%ScpPort% + +:TC14 + +echo: +echo ****************************************************************************************************************** +echo TC14 Inventory Management Certificates from SCP Location +echo ****************************************************************************************************************** +echo overwrite: %overwrite% +set storepath=/home/ec2-user +echo storepath: %storepath% +set casename=Inventory + +A10vThunderTestConsole.exe -clientmachine=%clientmachine% -casename=%casename% -user=%user% -password=%password% -storepath=%storepath% -devicegroup= -managementtype=%mgt% -certalias=%cert% -overwrite=%overwrite% -storetype=mgmt -orchtoscpserverip=%OrchToScpServerIp% -a10toscpserverip=%A10ToScpServerIp% -scpusername=%ScpUserName% -scppassword=%ScpPassword% -scpport=%ScpPort% + +@pause diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ddd5211 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# v3.0.0 +* Support for Partitions +* Support for Bindings +* Support for Management Certs +* Support for A10 V4,V6 and V7 Devices + +# v1.0.0 +* Initial Release diff --git a/Media/Images/AddPubCert.gif b/Media/Images/AddPubCert.gif deleted file mode 100644 index e6e3db1..0000000 Binary files a/Media/Images/AddPubCert.gif and /dev/null differ diff --git a/Media/Images/CertStore1.gif b/Media/Images/CertStore1.gif deleted file mode 100644 index 01f7b69..0000000 Binary files a/Media/Images/CertStore1.gif and /dev/null differ diff --git a/Media/Images/CertStore2.gif b/Media/Images/CertStore2.gif deleted file mode 100644 index e52ddbe..0000000 Binary files a/Media/Images/CertStore2.gif and /dev/null differ diff --git a/Media/Images/CertStoreType-Advanced.gif b/Media/Images/CertStoreType-Advanced.gif deleted file mode 100644 index 077fa44..0000000 Binary files a/Media/Images/CertStoreType-Advanced.gif and /dev/null differ diff --git a/Media/Images/CertStoreType-Basic.gif b/Media/Images/CertStoreType-Basic.gif deleted file mode 100644 index 3a21c8a..0000000 Binary files a/Media/Images/CertStoreType-Basic.gif and /dev/null differ diff --git a/Media/Images/CertStoreType-CustomFields.gif b/Media/Images/CertStoreType-CustomFields.gif deleted file mode 100644 index 9e0cd8f..0000000 Binary files a/Media/Images/CertStoreType-CustomFields.gif and /dev/null differ diff --git a/Media/Images/CertStoreType-EntryParameters.gif b/Media/Images/CertStoreType-EntryParameters.gif deleted file mode 100644 index 277e052..0000000 Binary files a/Media/Images/CertStoreType-EntryParameters.gif and /dev/null differ diff --git a/Media/Images/CertificateInventory.gif b/Media/Images/CertificateInventory.gif deleted file mode 100644 index 6544ffc..0000000 Binary files a/Media/Images/CertificateInventory.gif and /dev/null differ diff --git a/Media/Images/NewCertNewAlias.gif b/Media/Images/NewCertNewAlias.gif deleted file mode 100644 index 201db47..0000000 Binary files a/Media/Images/NewCertNewAlias.gif and /dev/null differ diff --git a/Media/Images/PubCertReplace.gif b/Media/Images/PubCertReplace.gif deleted file mode 100644 index a10a42d..0000000 Binary files a/Media/Images/PubCertReplace.gif and /dev/null differ diff --git a/Media/Images/RemoveCertAndKey.gif b/Media/Images/RemoveCertAndKey.gif deleted file mode 100644 index b8203ba..0000000 Binary files a/Media/Images/RemoveCertAndKey.gif and /dev/null differ diff --git a/Media/Images/RemovePubCert.gif b/Media/Images/RemovePubCert.gif deleted file mode 100644 index e50bc45..0000000 Binary files a/Media/Images/RemovePubCert.gif and /dev/null differ diff --git a/Media/Images/ReplaceCertSameAlias.gif b/Media/Images/ReplaceCertSameAlias.gif deleted file mode 100644 index 9253a82..0000000 Binary files a/Media/Images/ReplaceCertSameAlias.gif and /dev/null differ diff --git a/README.md b/README.md index 784bfcd..7796f14 100644 --- a/README.md +++ b/README.md @@ -1,193 +1,1045 @@ -# a10vThunder +

+ a10vThunder Universal Orchestrator Extension +

+ +

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

+ +

+ + + Support + + · + + Installation + + · + + License + + · + + Related Integrations + +

+ +## Overview + +A Keyfactor Universal Orchestrator extension for managing SSL/TLS certificates on A10 Networks vThunder load balancers through direct API integration and SCP-based management interface certificate deployment. + +The A10 vThunder Orchestrator provides automated certificate lifecycle management for A10 Networks vThunder appliances. It implements two distinct certificate store types: + +1. **ThunderSsl**: Direct API-based management of SSL certificates for load balancing and application delivery +2. **ThunderMgmt**: SCP-based management of certificates for the A10 management interface (GUI/API access) + +### Architecture + +The A10 vThunder Orchestrator implements two distinct certificate store types with different architectural approaches: +```mermaid +graph TB + subgraph "Keyfactor Environment" + KF[Keyfactor Command] + UO[Universal Orchestrator
with A10 Extension] + end + + subgraph "ThunderSsl Store Type - Direct API Management" + A10_SSL[A10 vThunder Appliance
API Endpoint] + SSL_STORE[(SSL Certificate Store
Certificates & Keys)] + SSL_TEMPLATES[SSL Templates
server-ssl / client-ssl] + VIRTUAL_SERVICES[Virtual Services
Load Balancer Config] + end + + subgraph "ThunderMgmt Store Type - SCP-Based Management" + SCP[SCP Server
Intermediate Storage] + A10_MGMT[A10 vThunder Appliance
Management Interface] + MGMT_STORE[(Management Certs
.crt / .key files)] + end -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. + KF -->|Certificate Lifecycle Jobs| UO -#### Integration status: Production - Ready for use in production environments. + UO -->|"1. AXAPI REST Calls
(HTTPS - v4/v6)
Auth, Upload, Template Updates"| A10_SSL + A10_SSL -->|Manages| SSL_STORE + A10_SSL -->|Updates Bindings| SSL_TEMPLATES + SSL_TEMPLATES -->|Bound To| VIRTUAL_SERVICES -## About the Keyfactor Universal Orchestrator Capability + UO -->|"2a. SCP Upload
(SSH/SCP)
cert.crt + cert.key"| SCP + SCP -->|"2b. A10 Retrieves
(SCP/SSH)"| A10_MGMT + UO -->|"2c. AXAPI Install Command
(HTTPS)"| A10_MGMT + A10_MGMT -->|Installs| MGMT_STORE -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. + style UO fill:#4CAF50,stroke:#2E7D32,color:#fff + style A10_SSL fill:#2196F3,stroke:#1565C0,color:#fff + style A10_MGMT fill:#2196F3,stroke:#1565C0,color:#fff + style SCP fill:#FF9800,stroke:#E65100,color:#fff + style SSL_STORE fill:#9C27B0,stroke:#6A1B9A,color:#fff + style MGMT_STORE fill:#9C27B0,stroke:#6A1B9A,color:#fff +``` -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. +#### ThunderSsl Store Type (Direct API Management) -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. +The ThunderSsl store type provides direct, API-based certificate management: +- **Single-hop architecture**: Orchestrator connects directly to A10 AXAPI (REST API) via HTTPS +- **Automatic template management**: Detects and updates SSL template bindings (server-ssl/client-ssl) +- **Zero-downtime replacements**: Creates timestamped certificates and atomically rebinds templates +- **Multi-tenant support**: Full partition support for isolated certificate operations +- **API version flexibility**: Automatically detects and supports both AXAPI v4 and v6 +#### ThunderMgmt Store Type (SCP-Based Management) -## Support for a10vThunder +The ThunderMgmt store type uses an intermediate SCP server for management interface certificates: -a10vThunder is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. +- **Three-party architecture**: Orchestrator → SCP Server → A10 Device +- **Network flexibility**: Supports different network paths between orchestrator and A10 device +- **File-based deployment**: Uploads .crt and .key files to SCP server for A10 retrieval +- **Management interface specific**: Used exclusively for A10 GUI/API access certificates +- **Coordinated installation**: AXAPI commands trigger certificate installation after file transfer -###### To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. -___ +Both store types support PAM integration for secure credential management and require appropriate A10 device permissions. +#### ThunderSsl Store Type +Uses A10's native REST API (AXAPI) for direct certificate management: +- Certificates are uploaded directly to the A10 appliance +- Supports both certificate-only and certificate-with-private-key operations +- Automatically detects and handles template bindings and virtual service configurations +- Implements intelligent certificate replacement to avoid service disruption +#### A10 vThunder Requirements +- A10 vThunder appliance with AXAPI support +- API versions 4.x or 6.x supported (automatically detected) +- Valid user account with certificate management privileges +- For ThunderMgmt: SSH/SCP access enabled + +#### Required A10 Permissions +The orchestrator requires an A10 user account with permissions to: +- Access AXAPI (REST API) +- Manage SSL certificates and private keys +- Read/write SSL templates (server-ssl and client-ssl) +- Query and modify virtual services +- Write configuration to memory +- Set active partitions +- For ThunderMgmt: SSH/SCP file operations + +### Key Features + +- **Direct SSL Certificate Management**: Native A10 API integration for SSL certificate deployment and management +- **Template-Aware Operations**: Intelligent handling of certificates bound to SSL templates and virtual services +- **Multi-API Version Support**: Automatic detection and support for A10 API v4 and v6 +- **Partition Support**: Full support for A10 partitions for multi-tenant deployments +- **Certificate Inventory**: Comprehensive discovery and inventory of existing certificates +- **Management Interface Certificates**: SCP-based deployment for A10 management interface certificates +- **PAM Integration**: Support for Privileged Access Management systems +- **Advanced Certificate Replacement**: Zero-downtime certificate replacement with automatic template rebinding + +The a10vThunder Universal Orchestrator extension implements 2 Certificate Store Types. Depending on your use case, you may elect to use one, or both of these Certificate Store Types. Descriptions of each are provided below. + +- [A10 Thunder Ssl Certificates](#ThunderSsl) + +- [A10 Thunder Management Certificates](#ThunderMgmt) + + +## Compatibility + +This integration is compatible with Keyfactor Universal Orchestrator version 10.4 and later. + +## Support +The a10vThunder 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. + +## Requirements & Prerequisites + +Before installing the a10vThunder 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. + + + +## Certificate Store Types + +To use the a10vThunder Universal Orchestrator extension, you **must** create the Certificate Store Types required for your use-case. This only needs to happen _once_ per Keyfactor Command instance. + +The a10vThunder Universal Orchestrator extension implements 2 Certificate Store Types. Depending on your use case, you may elect to use one, or both of these Certificate Store Types. + +### ThunderSsl + +
Click to expand details + + +#### 🔒 SSL Certificates + +**Purpose:** +Used for securing traffic that passes through the device (i.e., traffic handled by SLB/ADC features). + +**Usage Context:** +- SSL Offloading +- SSL Intercept (Decryption/Encryption) +- Reverse proxy configurations + +**Configured In:** +- **GUI:** `ADC → Ssl Management + + +**Example:** +If the A10 is acting as an SSL offloader for a backend web server, the **SSL Certificate** is used to terminate client HTTPS sessions. + + + + +#### A10 Thunder Ssl Certificates Requirements + +#### Creating a User for API Access on A10 vThunder + +This guide explains how to create a user on A10 vThunder for API (AXAPI) access with appropriate privileges. + +##### Step-by-Step Instructions + +1. **Enter configuration mode:** + ```bash + configure terminal + ``` + +2. **Create the user and set a password:** + ```bash + admin apiuser password yourStrongPassword + ``` + + Replace `apiuser` with the desired username, and `yourStrongPassword` with a secure password. + +3. **Assign necessary privileges:** + ```bash + privilege read + privilege write + privilege partition-enable-disable + privilege partition-read + privilege partition-write + ``` + + These privileges grant the user: + - Global read and write access + - Per-partition read and write access + - Permission to enable or disable partitions + +4. **(Optional) Enable external health monitor privilege (if needed):** + ```bash + privilege hm + ``` + +5. **Exit user configuration:** + ```bash + exit + ``` + +##### ThunderSsl Aliases + +In the ThunderSsl store type, the **alias** directly corresponds to the certificate and private key names stored on the A10 appliance: + +- **Certificate Name**: The alias becomes the SSL certificate identifier in A10's certificate store +- **Private Key Name**: The same alias is used for the associated private key +- **Template References**: SSL templates reference certificates by this exact alias name +- **API Operations**: All A10 API calls use this alias to identify the certificate/key pair + +###### Example ThunderSsl Usage +``` +Alias: "webserver-prod-2025" +→ A10 Certificate: "webserver-prod-2025" +→ A10 Private Key: "webserver-prod-2025" +→ Template Reference: server-ssl template uses cert "webserver-prod-2025" +``` + +###### Alias Renaming for Template-Bound Certificates + +When replacing a certificate that's bound to SSL templates, the orchestrator uses an intelligent renaming strategy: + +1. **Timestamp Generation**: Creates a Unix timestamp (10 digits) +2. **Alias Pattern Matching**: + - If alias contains existing timestamp: `webserver-prod_1640995200` → `webserver-prod_1672531200` + - If no timestamp found: `webserver-prod` → `webserver-prod_1672531200` +3. **Length Validation**: Ensures final alias stays within A10's 240-character limit +4. **Template Updates**: All SSL templates are updated to reference the new timestamped alias +5. **Cleanup**: Original certificate is removed after successful template updates + +###### Replacement Workflow Example +``` +Original: "api-gateway-cert" +Step 1: Generate new alias → "api-gateway-cert_1672531200" +Step 2: Upload certificate with new alias +Step 3: Update server-ssl templates: cert "api-gateway-cert" → "api-gateway-cert_1672531200" +Step 4: Update client-ssl templates: cert "api-gateway-cert" → "api-gateway-cert_1672531200" +Step 5: Remove old certificate "api-gateway-cert" +Step 6: Rebind templates to virtual services +``` + +###### Alias Best Practices +- Use descriptive names that indicate purpose: `web-frontend-ssl`, `api-backend-tls` +- Avoid special characters that might conflict with A10 naming rules +- Consider including environment indicators: `prod-web-cert`, `stage-api-cert` +- Remember that renaming will append timestamps for template-bound certificates + +###### Character Limitations +- **Maximum Length**: 240 characters (enforced by orchestrator) +- **Recommended Characters**: Letters, numbers, hyphens, underscores +- **Avoid**: Special characters that might cause issues in API calls or file operations + +##### Troubleshooting Alias Issues + +###### ThunderSsl Common Issues +- **Template Update Failures**: Verify templates exist and are accessible +- **Long Alias Names**: Orchestrator will truncate to fit timestamp if needed +- **Special Characters**: May cause API call failures + +##### Notes + +- This user will now be able to authenticate and perform actions via A10's AXAPI (v2/v3) interface. +- Role-Based Access (RBA) and partition assignment can further fine-tune access control. + +##### Example Login via AXAPI + +Example using `curl` for AXAPI v3 login: +```bash +curl -X POST https:///axapi/v3/auth \ + -d '{"credentials":{"username":"apiuser","password":"yourStrongPassword"}}' \ + -H "Content-Type: application/json" +``` + + + +#### Supported Operations + +| Operation | Is Supported | +|--------------|------------------------------------------------------------------------------------------------------------------------| +| Add | ✅ Checked | +| Remove | ✅ Checked | +| Discovery | 🔲 Unchecked | +| Reenrollment | 🔲 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 ThunderSsl kfutil details + + ##### Using online definition from GitHub: + This will reach out to GitHub and pull the latest store-type definition + ```shell + # A10 Thunder Ssl Certificates + kfutil store-types create ThunderSsl + ``` + + ##### Offline creation using integration-manifest file: + If required, it is possible to create store types from the [integration-manifest.json](./integration-manifest.json) included in this repo. + You would first download the [integration-manifest.json](./integration-manifest.json) and then run the following command + in your offline environment. + ```shell + kfutil store-types create --from-file integration-manifest.json + ``` +
+ + +#### Manual Creation +Below are instructions on how to create the ThunderSsl store type manually in +the Keyfactor Command Portal +
Click to expand manual ThunderSsl details + + Create a store type called `ThunderSsl` with the attributes in the tables below: + + ##### Basic Tab + | Attribute | Value | Description | + | --------- | ----- | ----- | + | Name | A10 Thunder Ssl Certificates | Display name for the store type (may be customized) | + | Short Name | ThunderSsl | Short display name for the store type | + | Capability | ThunderSsl | 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 Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | + | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | + | Needs Server | ✅ Checked | Determines if a target server name is required when creating store | + | Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint | + | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell | + | Requires Store Password | 🔲 Unchecked | Enables users to optionally specify a store password when defining a Certificate Store. | + | Supports Entry Password | 🔲 Unchecked | Determines if an individual entry within a store can have a password. | + + The Basic tab should look like this: + + ![ThunderSsl Basic Tab](docsource/images/ThunderSsl-basic-store-type-dialog.png) + + ##### Advanced Tab + | Attribute | Value | Description | + | --------- | ----- | ----- | + | Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. | + | Private Key Handling | Optional | 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. | + | 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: + + ![ThunderSsl Advanced Tab](docsource/images/ThunderSsl-advanced-store-type-dialog.png) + + > 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. + + ##### Custom Fields Tab + Custom fields operate at the certificate store level and are used to control how the orchestrator connects to the remote target server containing the certificate store to be managed. The following custom fields should be added to the store type: + + | Name | Display Name | Description | Type | Default Value/Options | Required | + | ---- | ------------ | ---- | --------------------- | -------- | ----------- | + | allowInvalidCert | Allow Invalid Cert on A10 Management API | Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections. | Bool | true | ✅ Checked | + + The Custom Fields tab should look like this: + + ![ThunderSsl Custom Fields Tab](docsource/images/ThunderSsl-custom-fields-store-type-dialog.png) + + + ###### Allow Invalid Cert on A10 Management API + Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections. + + ![ThunderSsl Custom Field - allowInvalidCert](docsource/images/ThunderSsl-custom-field-allowInvalidCert-dialog.png) + + + + + +
+
+ +### ThunderMgmt + +
Click to expand details + + +#### 🔐 Management Certificates + +**Purpose:** +Used to secure HTTPS access to the A10 management interface (GUI/API). + +**Usage Context:** +- AXAPI (API access over HTTPS) +- Web GUI login +- Any administrative HTTPS session + +**Configured In:** +- **GUI:** `System → Settings → Certificate` + +**Example:** +When a user logs into the GUI via `https://`, the certificate presented is the **Management Certificate**. + + + + +#### A10 Thunder Management Certificates Requirements + +#### A10 Certificate Management Orchestrator Extension + +This orchestrator extension automates the process of uploading, inventorying, and deploying SSL certificates from a Linux SCP server to an A10 vThunder device. Due to A10 API limitations, certificates must be pulled from the SCP server directly by the A10 device itself. + +--- + +##### 📌 How It Works + +1. **The orchestrator** connects to a Linux server via SCP to inventory available certificates. +2. It stores relevant metadata and pushes new certificates and keys to the SCP server. +3. It then instructs the **A10 device** to retrieve the certificate and private key from the Linux server using API calls. +4. The A10 device loads the certificate and key directly from the SCP server for use on its **management interface**. + +--- + +##### 📡 API Call Example (From A10 Device) + +```http +POST /axapi/v3/web-service/secure/certificate +``` + +**Payload:** +```json +{ + "certificate": { + "load": 1, + "file-url": "scp://ec2-user:dda@172.31.93.107:/home/ec2-user/26125.crt" + } +} +``` + +> A similar call is made for loading the private key onto the A10 device using a separate AXAPI endpoint. + +- The A10 device **must have access** to the SCP server via the specified IP (`A10ToScpServerIp`). +- Ensure the certificate and key file paths are correct and accessible to the SCP user. --- +##### 🔐 Linux Server Requirements + +###### User Access +- The SCP user (`ScpUserName`, e.g., `ec2-user`) must: + - Have SSH/SCP access. + - Authenticate with a password. + - Have **read and write** permissions in the SCP location. +> New certificates and **private keys** are generated by Keyfactor and uploaded to this location by the orchestrator. Therefore, write access is essential. +###### SCP Directory Permissions +- Ensure the directory (e.g., `/home/ec2-user/`) is: + - Writable by the orchestrator (to upload new certs/keys). + - Readable by both the orchestrator and the A10 device (via SCP). + +--- -## Platform Specific Notes +##### 🔄 Alternate Design Consideration -The minimum version of the Universal Orchestrator Framework needed to run this version of the extension is +It may be possible to use the A10 device itself as the SCP target location if it supports read/write SCP operations **outside the CLI context**. However, A10 devices typically restrict file access through CLI or API mechanisms only, and not through standard SCP server operations. This limitation is why a separate Linux SCP server is currently required. -The Keyfactor Universal Orchestrator may be installed on either Windows or Linux based platforms. The certificate operations supported by a capability may vary based what platform the capability is installed on. The table below indicates what capabilities are supported based on which platform the encompassing Universal Orchestrator is running. -| Operation | Win | Linux | -|-----|-----|------| -|Supports Management Add|✓ |✓ | -|Supports Management Remove|✓ |✓ | -|Supports Create Store| | | -|Supports Discovery| | | -|Supports Renrollment| | | -|Supports Inventory|✓ |✓ | +--- +##### 🔓 Network and Port Requirements +| Source | Destination | Port | Protocol | Purpose | +|--------------------|---------------------|------|----------|-------------------------------| +| Orchestrator | Linux SCP Server | 22 | TCP | Inventory and upload via SCP | +| A10 Device | Linux SCP Server | 22 | TCP | Cert and key retrieval via SCP| +| Orchestrator/Admin | A10 Device (API) | 443 | HTTPS | API calls to load certificate | --- +##### ThunderMgmt Aliases + +In the ThunderMgmt store type, the **alias** determines the filename for certificates stored on the SCP server: + +- **Certificate File**: `{alias}.crt` on the SCP server +- **Private Key File**: `{alias}.key` on the SCP server +- **A10 API Reference**: The A10 management interface loads certificates using SCP URLs pointing to these files + +###### Example ThunderMgmt Usage +``` +Alias: "mgmt-interface-cert" +→ SCP Server Files: + - /home/scpuser/mgmt-interface-cert.crt + - /home/scpuser/mgmt-interface-cert.key +→ A10 API Call: + - Certificate URL: scp://scpuser:pass@192.168.1.100:/home/scpuser/mgmt-interface-cert.crt + - Key URL: scp://scpuser:pass@192.168.1.100:/home/scpuser/mgmt-interface-cert.key +``` + +###### For Alias Names +- Use names that clearly identify the management purpose: `mgmt-interface-2025` +- Ensure filenames are valid for both SCP server filesystem and A10 API calls +- Consider including renewal dates: `mgmt-cert-jan2025` + +###### ThunderMgmt File Management + +The orchestrator handles file operations as follows: + +1. **Add Operation**: + - Uploads `{alias}.crt` and `{alias}.key` to SCP server + - Calls A10 API to load certificate from SCP URLs + - A10 device pulls files directly from SCP server + +2. **Remove Operation**: + - Deletes `{alias}.crt` and `{alias}.key` from SCP server + - Does not modify A10 management interface configuration + +3. **Replace Operation** (with Overwrite=true): + - Overwrites existing `{alias}.crt` and `{alias}.key` files + - Calls A10 API to reload certificate from same SCP URLs + +###### Character Limitations +- **Maximum Length**: 240 characters (enforced by orchestrator) +- **Recommended Characters**: Letters, numbers, hyphens, underscores +- **Avoid**: Special characters that might cause issues in API calls or file operations + +###### ThunderMgmt Common Issues +- **File Path Issues**: Ensure SCP user has access to the target directory +- **Invalid Filenames**: Some characters may not be valid for filesystem operations +- **URL Encoding**: Special characters in aliases may require URL encoding in SCP URLs + +##### ✅ Summary + +This extension coordinates certificate and private key delivery by using SCP as a bridge between orchestrator logic and A10's strict API requirements. It ensures secure and automated deployment for the management interface certificates with minimal manual intervention. + + + +#### Supported Operations + +| Operation | Is Supported | +|--------------|------------------------------------------------------------------------------------------------------------------------| +| Add | ✅ Checked | +| Remove | ✅ Checked | +| Discovery | 🔲 Unchecked | +| Reenrollment | 🔲 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 ThunderMgmt kfutil details + + ##### Using online definition from GitHub: + This will reach out to GitHub and pull the latest store-type definition + ```shell + # A10 Thunder Management Certificates + kfutil store-types create ThunderMgmt + ``` + + ##### Offline creation using integration-manifest file: + If required, it is possible to create store types from the [integration-manifest.json](./integration-manifest.json) included in this repo. + You would first download the [integration-manifest.json](./integration-manifest.json) and then run the following command + in your offline environment. + ```shell + kfutil store-types create --from-file integration-manifest.json + ``` +
+ + +#### Manual Creation +Below are instructions on how to create the ThunderMgmt store type manually in +the Keyfactor Command Portal +
Click to expand manual ThunderMgmt details + + Create a store type called `ThunderMgmt` with the attributes in the tables below: + + ##### Basic Tab + | Attribute | Value | Description | + | --------- | ----- | ----- | + | Name | A10 Thunder Management Certificates | Display name for the store type (may be customized) | + | Short Name | ThunderMgmt | Short display name for the store type | + | Capability | ThunderMgmt | 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 Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | + | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | + | Needs Server | ✅ Checked | Determines if a target server name is required when creating store | + | Blueprint Allowed | 🔲 Unchecked | Determines if store type may be included in an Orchestrator blueprint | + | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell | + | Requires Store Password | 🔲 Unchecked | Enables users to optionally specify a store password when defining a Certificate Store. | + | Supports Entry Password | 🔲 Unchecked | Determines if an individual entry within a store can have a password. | + + The Basic tab should look like this: + + ![ThunderMgmt Basic Tab](docsource/images/ThunderMgmt-basic-store-type-dialog.png) + + ##### Advanced Tab + | Attribute | Value | Description | + | --------- | ----- | ----- | + | Supports Custom Alias | Required | 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. | + | 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: + + ![ThunderMgmt Advanced Tab](docsource/images/ThunderMgmt-advanced-store-type-dialog.png) + + > 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. + + ##### Custom Fields Tab + Custom fields operate at the certificate store level and are used to control how the orchestrator connects to the remote target server containing the certificate store to be managed. The following custom fields should be added to the store type: + + | Name | Display Name | Description | Type | Default Value/Options | Required | + | ---- | ------------ | ---- | --------------------- | -------- | ----------- | + | OrchToScpServerIp | Orch To Scp Server Ip | IP address or hostname of the SCP server that the Universal Orchestrator will connect to for uploading certificate files. This SCP server acts as an intermediary storage location before the A10 device retrieves the certificates. | String | | ✅ Checked | + | ScpPort | Port Used For Scp | TCP port number used for SSH/SCP connections to the SCP server. Typically port 22 for standard SSH/SCP operations. | String | | ✅ Checked | + | ScpUserName | UserName Used For Scp | Username credential for authenticating to the SCP server. This account must have write permissions to the target directory path specified in the certificate store configuration. Supports PAM integration for secure credential retrieval. | Secret | | ✅ Checked | + | ScpPassword | Password Used For Scp | Password credential for authenticating to the SCP server. Used in conjunction with ScpUserName for SSH/SCP authentication. Supports PAM integration for secure credential retrieval. | Secret | | ✅ Checked | + | A10ToScpServerIp | A10 Device To Scp Server Ip | IP address or hostname that the A10 vThunder device uses to connect to the SCP server for retrieving certificate files. This may differ from OrchToScpServerIp due to network topology, routing, or firewall configurations where the A10 device and orchestrator access the SCP server through different network paths. | String | | ✅ Checked | + | allowInvalidCert | Allow Invalid Cert on A10 Management API | Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections used during the certificate installation process. | Bool | true | ✅ Checked | + + The Custom Fields tab should look like this: + + ![ThunderMgmt Custom Fields Tab](docsource/images/ThunderMgmt-custom-fields-store-type-dialog.png) + + + ###### Orch To Scp Server Ip + IP address or hostname of the SCP server that the Universal Orchestrator will connect to for uploading certificate files. This SCP server acts as an intermediary storage location before the A10 device retrieves the certificates. + + ![ThunderMgmt Custom Field - OrchToScpServerIp](docsource/images/ThunderMgmt-custom-field-OrchToScpServerIp-dialog.png) + + + + ###### Port Used For Scp + TCP port number used for SSH/SCP connections to the SCP server. Typically port 22 for standard SSH/SCP operations. + + ![ThunderMgmt Custom Field - ScpPort](docsource/images/ThunderMgmt-custom-field-ScpPort-dialog.png) + + + + ###### UserName Used For Scp + Username credential for authenticating to the SCP server. This account must have write permissions to the target directory path specified in the certificate store configuration. Supports PAM integration for secure credential retrieval. + + ![ThunderMgmt Custom Field - ScpUserName](docsource/images/ThunderMgmt-custom-field-ScpUserName-dialog.png) + + + + ###### Password Used For Scp + Password credential for authenticating to the SCP server. Used in conjunction with ScpUserName for SSH/SCP authentication. Supports PAM integration for secure credential retrieval. + + ![ThunderMgmt Custom Field - ScpPassword](docsource/images/ThunderMgmt-custom-field-ScpPassword-dialog.png) + + + + ###### A10 Device To Scp Server Ip + IP address or hostname that the A10 vThunder device uses to connect to the SCP server for retrieving certificate files. This may differ from OrchToScpServerIp due to network topology, routing, or firewall configurations where the A10 device and orchestrator access the SCP server through different network paths. + + ![ThunderMgmt Custom Field - A10ToScpServerIp](docsource/images/ThunderMgmt-custom-field-A10ToScpServerIp-dialog.png) + + + + ###### Allow Invalid Cert on A10 Management API + Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections used during the certificate installation process. + + ![ThunderMgmt Custom Field - allowInvalidCert](docsource/images/ThunderMgmt-custom-field-allowInvalidCert-dialog.png) + + + + + +
+
+ + +## Installation + +1. **Download the latest a10vThunder Universal Orchestrator extension from GitHub.** + + Navigate to the [a10vThunder Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/a10vthunder-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. + + | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `a10vthunder-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` | + + 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`. + +2. **Locate the Universal Orchestrator extensions directory.** + + * **Default on Windows** - `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions` + * **Default on Linux** - `/opt/keyfactor/orchestrator/extensions` + +3. **Create a new directory for the a10vThunder Universal Orchestrator extension inside the extensions directory.** + + Create a new directory called `a10vthunder-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 `a10vthunder-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 a10vThunder 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 a10vThunder Universal Orchestrator extension implements 2 Certificate Store Types, each of which implements different functionality. Refer to the individual instructions below for each Certificate Store Type that you deemed necessary for your use case from the installation section. + +
A10 Thunder Ssl Certificates (ThunderSsl) + +### ⚙️ Configuration Fields + +| Name | Display Name | Description | Type | Required | +|-------------------|-------------------------------|--------------------------------------------------------------|--------|----------| +| allowInvalidCert | Allow Invalid Cert on A10 API | If true, allows self-signed/untrusted certs for A10 API access | Bool | ✅ (default: true) | + + +### Store Creation + +#### Manually with the Command UI + +
Click to expand details + +1. **Navigate to the _Certificate Stores_ page in Keyfactor Command.** + + Log into Keyfactor Command, toggle the _Locations_ dropdown, and click _Certificate Stores_. + +2. **Add a Certificate Store.** + + Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. + + | Attribute | Description | + | --------- |---------------------------------------------------------| + | Category | Select "A10 Thunder Ssl Certificates" or the customized certificate store name from the previous step. | + | Container | Optional container to associate certificate store with. | + | Client Machine | Hostname or IP address of the A10 vThunder appliance to be managed. The orchestrator will establish an AXAPI (REST API) connection using the credentials specified in the Server Username and Server Password fields to manage SSL certificates directly on the device. | + | Store Path | A10 partition name where certificates will be managed. Use 'shared' for the default shared partition, or specify a custom partition name (e.g., 'tenant-prod') for multi-tenant deployments. The partition must already exist on the A10 device. Leave empty to default to the shared partition. | + | Orchestrator | Select an approved orchestrator capable of managing `ThunderSsl` certificates. Specifically, one with the `ThunderSsl` capability. | + | allowInvalidCert | Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections. | + +
+ + + +#### Using kfutil CLI + +
Click to expand details + +1. **Generate a CSV template for the ThunderSsl certificate store** + + ```shell + kfutil stores import generate-template --store-type-name ThunderSsl --outpath ThunderSsl.csv + ``` +2. **Populate the generated CSV file** + + Open the CSV file, and reference the table below to populate parameters for each **Attribute**. + + | Attribute | Description | + | --------- | ----------- | + | Category | Select "A10 Thunder Ssl Certificates" or the customized certificate store name from the previous step. | + | Container | Optional container to associate certificate store with. | + | Client Machine | Hostname or IP address of the A10 vThunder appliance to be managed. The orchestrator will establish an AXAPI (REST API) connection using the credentials specified in the Server Username and Server Password fields to manage SSL certificates directly on the device. | + | Store Path | A10 partition name where certificates will be managed. Use 'shared' for the default shared partition, or specify a custom partition name (e.g., 'tenant-prod') for multi-tenant deployments. The partition must already exist on the A10 device. Leave empty to default to the shared partition. | + | Orchestrator | Select an approved orchestrator capable of managing `ThunderSsl` certificates. Specifically, one with the `ThunderSsl` capability. | + | Properties.allowInvalidCert | Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections. | + +3. **Import the CSV file to create the certificate stores** + + ```shell + kfutil stores import csv --store-type-name ThunderSsl --file ThunderSsl.csv + ``` + +
+ + +#### PAM Provider Eligible Fields +
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator + +If a PAM provider was installed _on the Universal Orchestrator_ in the [Installation](#Installation) section, the following parameters can be configured for retrieval _on the Universal Orchestrator_. + + | Attribute | Description | + | --------- | ----------- | + | ServerUsername | Username to use when connecting to server | + | ServerPassword | Password to use when connecting to server | + +Please refer to the **Universal Orchestrator (remote)** usage section ([PAM providers on the Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam)) for your selected PAM provider for instructions on how to load attributes orchestrator-side. +> Any secret can be rendered by a PAM provider _installed on the Keyfactor Command server_. The above parameters are specific to attributes that can be fetched by an installed PAM provider running on the Universal Orchestrator server itself. + +
+ + +> 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). + + +
+ +
A10 Thunder Management Certificates (ThunderMgmt) + + +### Store Creation + +#### Manually with the Command UI + +
Click to expand details + +1. **Navigate to the _Certificate Stores_ page in Keyfactor Command.** + + Log into Keyfactor Command, toggle the _Locations_ dropdown, and click _Certificate Stores_. + +2. **Add a Certificate Store.** + + Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. + + | Attribute | Description | + | --------- |---------------------------------------------------------| + | Category | Select "A10 Thunder Management Certificates" or the customized certificate store name from the previous step. | + | Container | Optional container to associate certificate store with. | + | Client Machine | Hostname or IP address of the A10 vThunder appliance to be managed. The orchestrator will establish an AXAPI (REST API) connection using the credentials specified in the Server Username and Server Password fields to trigger certificate installation on the management interface after uploading files via SCP. | + | Store Path | Absolute directory path on the SCP server where certificate files (.crt and .key) will be uploaded. The A10 device will retrieve certificate files from this location. Example: '/home/certuser'. The specified path must exist and the SCP user must have write permissions to this directory. | + | Orchestrator | Select an approved orchestrator capable of managing `ThunderMgmt` certificates. Specifically, one with the `ThunderMgmt` capability. | + | OrchToScpServerIp | IP address or hostname of the SCP server that the Universal Orchestrator will connect to for uploading certificate files. This SCP server acts as an intermediary storage location before the A10 device retrieves the certificates. | + | ScpPort | TCP port number used for SSH/SCP connections to the SCP server. Typically port 22 for standard SSH/SCP operations. | + | ScpUserName | Username credential for authenticating to the SCP server. This account must have write permissions to the target directory path specified in the certificate store configuration. Supports PAM integration for secure credential retrieval. | + | ScpPassword | Password credential for authenticating to the SCP server. Used in conjunction with ScpUserName for SSH/SCP authentication. Supports PAM integration for secure credential retrieval. | + | A10ToScpServerIp | IP address or hostname that the A10 vThunder device uses to connect to the SCP server for retrieving certificate files. This may differ from OrchToScpServerIp due to network topology, routing, or firewall configurations where the A10 device and orchestrator access the SCP server through different network paths. | + | allowInvalidCert | Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections used during the certificate installation process. | + +
+ + + +#### Using kfutil CLI + +
Click to expand details -**A10 Networks vThunder Orchestrator** +1. **Generate a CSV template for the ThunderMgmt certificate store** -**Overview** + ```shell + kfutil stores import generate-template --store-type-name ThunderMgmt --outpath ThunderMgmt.csv + ``` +2. **Populate the generated CSV file** -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. + Open the CSV file, and reference the table below to populate parameters for each **Attribute**. -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. + | Attribute | Description | + | --------- | ----------- | + | Category | Select "A10 Thunder Management Certificates" or the customized certificate store name from the previous step. | + | Container | Optional container to associate certificate store with. | + | Client Machine | Hostname or IP address of the A10 vThunder appliance to be managed. The orchestrator will establish an AXAPI (REST API) connection using the credentials specified in the Server Username and Server Password fields to trigger certificate installation on the management interface after uploading files via SCP. | + | Store Path | Absolute directory path on the SCP server where certificate files (.crt and .key) will be uploaded. The A10 device will retrieve certificate files from this location. Example: '/home/certuser'. The specified path must exist and the SCP user must have write permissions to this directory. | + | Orchestrator | Select an approved orchestrator capable of managing `ThunderMgmt` certificates. Specifically, one with the `ThunderMgmt` capability. | + | Properties.OrchToScpServerIp | IP address or hostname of the SCP server that the Universal Orchestrator will connect to for uploading certificate files. This SCP server acts as an intermediary storage location before the A10 device retrieves the certificates. | + | Properties.ScpPort | TCP port number used for SSH/SCP connections to the SCP server. Typically port 22 for standard SSH/SCP operations. | + | Properties.ScpUserName | Username credential for authenticating to the SCP server. This account must have write permissions to the target directory path specified in the certificate store configuration. Supports PAM integration for secure credential retrieval. | + | Properties.ScpPassword | Password credential for authenticating to the SCP server. Used in conjunction with ScpUserName for SSH/SCP authentication. Supports PAM integration for secure credential retrieval. | + | Properties.A10ToScpServerIp | IP address or hostname that the A10 vThunder device uses to connect to the SCP server for retrieving certificate files. This may differ from OrchToScpServerIp due to network topology, routing, or firewall configurations where the A10 device and orchestrator access the SCP server through different network paths. | + | Properties.allowInvalidCert | Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections used during the certificate installation process. | +3. **Import the CSV file to create the certificate stores** -**A10 vThunder Configuration** + ```shell + kfutil stores import csv --store-type-name ThunderMgmt --file ThunderMgmt.csv + ``` -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: +#### PAM Provider Eligible Fields +
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator -#### 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|Unchecked -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 +If a PAM provider was installed _on the Universal Orchestrator_ in the [Installation](#Installation) section, the following parameters can be configured for retrieval _on the Universal Orchestrator_. -**Basic Settings:** + | Attribute | Description | + | --------- | ----------- | + | ServerUsername | Username to use when connecting to server | + | ServerPassword | Password to use when connecting to server | + | ScpUserName | Username credential for authenticating to the SCP server. This account must have write permissions to the target directory path specified in the certificate store configuration. Supports PAM integration for secure credential retrieval. | + | ScpPassword | Password credential for authenticating to the SCP server. Used in conjunction with ScpUserName for SSH/SCP authentication. Supports PAM integration for secure credential retrieval. | -![](Media/Images/CertStoreType-Basic.gif) +Please refer to the **Universal Orchestrator (remote)** usage section ([PAM providers on the Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam)) for your selected PAM provider for instructions on how to load attributes orchestrator-side. +> Any secret can be rendered by a PAM provider _installed on the Keyfactor Command server_. The above parameters are specific to attributes that can be fetched by an installed PAM provider running on the Universal Orchestrator server itself. -**Advanced Settings:** +
-![](Media/Images/CertStoreType-Advanced.gif) -**Custom Fields:** +> 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). -![](Media/Images/CertStoreType-CustomFields.gif) -**Entry Params:** +
-![](Media/Images/CertStoreType-EntryParameters.gif) -**2. Register the A10 vThunder Orchestrator with Keyfactor** -1. Stop the Keyfactor Universal Orchestrator Service for the orchestrator you plan to install this extension to run on. -2. In the Keyfactor Orchestrator installation folder (by convention usually C:\Program Files\Keyfactor\Keyfactor Orchestrator), find the "extensions" folder. Underneath that, create a new folder named "vThunderU". You may choose to use a different name, but then you must edit the manifest.json file downloaded from GitHub (Step 3) and modify based on Step 5 below. -3. Download the latest version of the A10 orchestrator extension from [GitHub](https://github.com/Keyfactor/a10vthunder-orchestrator). -4. Copy the contents of the download installation zip file to the folder created in Step 2. -5. (Optional) If you decided to name the folder created in Step 2 to something different than the suggested name (vThunderU), you will need to edit the manifest.json file. Modify "CertStores.{folder name}.Capability" to the folder name you created in Step 2. -6. Start the Keyfactor Universal Orchestrator Service. -Please reference the "Keyfactor Orchestrators Installation and Configuration Guide" obtainable from your Keyfactor contact/representative for more information regarding this step. +## API Integration Details -**3. Create a A10 vThunder Certificate Store within Keyfactor Command** -In Keyfactor Command create a new Certificate Store similar to the one below +### AXAPI Endpoints Used -![](Media/Images/CertStore1.gif) -![](Media/Images/CertStore2.gif) +- **Authentication**: `/axapi/v3/auth` or `/axapi/v4/auth` +- **SSL Certificates**: `/axapi/v3/slb/ssl-cert` or `/axapi/v4/slb/ssl-cert` +- **Private Keys**: `/axapi/v3/slb/ssl-key` or `/axapi/v4/slb/ssl-key` +- **SSL Templates**: `/axapi/v3/slb/template/server-ssl` and `/axapi/v3/slb/template/client-ssl` +- **Virtual Services**: `/axapi/v3/slb/virtual-server` +- **Partitions**: `/axapi/v3/active-partition` +- **Memory Operations**: `/axapi/v3/write/memory` -#### 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. +### Advanced Features -*** +#### Partition Support -#### Usage +The orchestrator fully supports A10 partitions: +- Set active partition before operations +- Isolate certificate operations to specific partitions +- Support multi-tenant deployments -**Adding New Certificate New Alias** +#### Template Management -![](Media/Images/NewCertNewAlias.gif) +Intelligent SSL template handling: +- Detection of server-ssl and client-ssl template usage +- Atomic template updates during certificate replacement +- Preservation of template configurations -*** +#### Virtual Service Coordination -**Replace Cert With Same Alias** +Advanced virtual service management: +- Mapping of templates to virtual service ports +- Coordinated unbinding and rebinding operations +- Support for multiple template types on single ports -![](Media/Images/ReplaceCertSameAlias.gif) +### TEST CASES -*** +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +1|Fresh Add SSL Certificate With Private Key|Will create new SSL certificate and private key on the vThunder appliance in shared partition|shared|true|WebServerSSL|The new WebServerSSL certificate and private key will be created in SSL certificate store on vThunder|True +1a|Replace SSL Cert with no overwrite flag|Should warn user that a cert cannot be replaced with the same name without overwrite flag|shared|false|WebServerSSL|Error message indicating overwrite flag must be used|True +1b|Replace SSL Cert with overwrite flag (unbound)|Will replace certificate and private key on vThunder for unbound certificate|shared|true|WebServerSSL|Certificate will be removed and re-added because it's not bound to templates|True +2|Add SSL Cert Without Private Key|This will create a certificate with no private key on vThunder|shared|false|PublicCertOnly|Only certificate will be added to vThunder SSL store with no private key|True +2a|Replace SSL Cert Without Private Key|This will replace a certificate with no private key on vThunder|shared|true|PublicCertOnly|Only certificate will be replaced on vThunder with no private key|True +2b|Replace SSL Cert Without Private Key no overwrite flag|Should warn user that a cert cannot be replaced with the same name without overwrite flag|shared|false|PublicCertOnly|Error message indicating overwrite flag must be used|True +3|Remove Unbound SSL Certificate and Private Key|Certificate and Private Key will be removed from A10|shared|N/A|WebServerSSL|Certificate and key will be removed from vThunder SSL store|True +3a|Remove SSL Certificate without Private Key|Certificate will be removed from A10|shared|N/A|PublicCertOnly|Certificate will be removed from vThunder SSL store|True -**Add Cert No Private Key** +### Template-Bound Certificate Operations -![](Media/Images/AddPubCert.gif) +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +4|Replace Server-SSL Template-Bound Certificate|Will create new timestamped certificate and update server-ssl templates|shared|true|APIGatewayCert|New certificate created with timestamp alias (APIGatewayCert_1672531200), server-ssl templates updated, virtual services rebound, old cert removed|True +4a|Replace Client-SSL Template-Bound Certificate|Will create new timestamped certificate and update client-ssl templates|shared|true|ClientAuthCert|New certificate created with timestamp alias (ClientAuthCert_1672531200), client-ssl templates updated, virtual services rebound, old cert removed|True +4b|Replace Multi-Template-Bound Certificate|Will create new timestamped certificate and update both server-ssl and client-ssl templates|shared|true|DualPurposeCert|New certificate created with timestamp, both template types updated with consistent alias, all virtual services rebound|True +4c|Attempt to Remove Template-Bound Certificate|Should fail with informative error about certificate being in use|shared|N/A|BoundServerCert|Error indicating certificate is bound to SSL templates and cannot be removed|True -*** +### Partition Operations -**Replace Cert No Private Key** +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +5|Add SSL Certificate to Custom Partition|Certificate will be added to specified partition instead of shared|tenant-prod|false|TenantWebCert|Certificate added to "tenant-prod" partition, isolated from shared partition|True +5a|Remove SSL Certificate from Custom Partition|Certificate will be removed from specified partition|tenant-prod|N/A|TenantWebCert|Certificate removed from "tenant-prod" partition, shared partition unaffected|True +5b|Replace Certificate in Custom Partition with Template Binding|Certificate replacement with template updates in specific partition|tenant-prod|true|TenantAPICert|New timestamped certificate created in partition, partition-specific templates updated|True -![](Media/Images/PubCertReplace.gif) +### Inventory Operations -*** +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +6|Inventory SSL Certificates from Shared Partition|Inventory of SSL certificates will be pulled from shared partition|shared|N/A|N/A|All SSL certificates in shared partition inventoried with private key flags and metadata|True +6a|Inventory SSL Certificates from Custom Partition|Inventory of SSL certificates will be pulled from specified partition|tenant-prod|N/A|N/A|All SSL certificates in "tenant-prod" partition inventoried, isolated from other partitions|True +6b|Inventory Mixed Certificate Types|Inventory should handle certificates with and without private keys|shared|N/A|N/A|Certificates with private keys marked as PrivateKeyEntry=true, certificates without marked as false|True -**Remove Cert No Private Key** +### API Version Compatibility -![](Media/Images/RemovePubCert.gif) +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +7|API v4 Detection and Template Operations|System should detect A10 software version 4.x and use appropriate API format for template updates|shared|true|V4TestCert|API v4 format detected and used for template updates, version info logged showing 4.x software|True +7a|API v6 Detection and Template Operations|System should detect A10 software version 6.x and use appropriate API format for template updates|shared|true|V6TestCert|API v6 format detected and used for template updates (default), version info logged|True -*** +## ThunderMgmt Store Type Test Cases -**Remove Cert and Private Key** +### SCP Certificate Operations -![](Media/Images/RemoveCertAndKey.gif) +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +8|Fresh Add Management Certificate via SCP|Will upload certificate files to SCP server and install on A10 management interface|/home/certuser|true|MgmtInterface2025|Files MgmtInterface2025.crt and MgmtInterface2025.key created on SCP server, A10 loads certificate via API|True +8a|Replace Management Certificate with overwrite flag|Will replace existing certificate files and reload on A10 management interface|/home/certuser|true|MgmtInterface2025|Existing files overwritten, A10 reloads certificate, 60-second delay observed, memory written|True +8b|Replace Management Certificate without overwrite flag|Should warn user that files cannot be replaced without overwrite flag|/home/certuser|false|MgmtInterface2025|Error indicating files exist and overwrite flag must be used|True +9|Add Management Cert Without Private Key|This will create certificate file only on SCP server|/home/certuser|false|MgmtCertOnly|Only .crt file will be created on SCP server, no .key file, A10 API called for certificate only|True +10|Remove Management Certificate Files|Certificate files will be removed from SCP server|/home/certuser|N/A|MgmtInterface2025|Both .crt and .key files deleted from SCP server, A10 management configuration unchanged|True -*** +### SCP Server Connectivity and Error Handling -**Certificate Inventory** +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +11|Inventory Management Certificates from SCP|Inventory of certificate files will be retrieved from SCP server directory|/home/certuser|N/A|N/A|All valid PEM certificates in SCP directory inventoried, invalid files skipped gracefully|True +11a|SCP Authentication Failure|Should handle SCP authentication errors gracefully|/home/certuser|N/A|TestCert|Clear authentication error message, operation fails safely, security not compromised|True +11b|SCP Network Connectivity Issues|Should handle network connectivity issues to SCP server|/home/unreachable|N/A|TestCert|Network timeout error captured, distinguishes from authentication errors, provides troubleshooting guidance|True +11c|Remote File Already Exists Check|Should properly detect existing files on SCP server before upload|/home/certuser|false|ExistingCert|File existence check works correctly, appropriate error when overwrite=false and file exists|True -![](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 +## 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 diff --git a/Setup/InstallA10AzureVM.ps1 b/Setup/InstallA10AzureVM.ps1 deleted file mode 100644 index b3cdce5..0000000 --- a/Setup/InstallA10AzureVM.ps1 +++ /dev/null @@ -1,66 +0,0 @@ -$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/a10vthunder-orchestrator.csproj b/a10vthunder-orchestrator/A10vThunder.csproj similarity index 62% rename from a10vthunder-orchestrator/a10vthunder-orchestrator.csproj rename to a10vthunder-orchestrator/A10vThunder.csproj index 02e9deb..b5e5865 100644 --- a/a10vthunder-orchestrator/a10vthunder-orchestrator.csproj +++ b/a10vthunder-orchestrator/A10vThunder.csproj @@ -1,9 +1,10 @@ - netcoreapp3.1 + true + net6.0;net8.0 true - a10vthunder_orchestrator + disable @@ -11,10 +12,11 @@ - - + + + diff --git a/a10vthunder-orchestrator/AnyErrors.cs b/a10vthunder-orchestrator/AnyErrors.cs index 0a7d506..4558a18 100644 --- a/a10vthunder-orchestrator/AnyErrors.cs +++ b/a10vthunder-orchestrator/AnyErrors.cs @@ -1,9 +1,23 @@ -using System; +// 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; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; -namespace a10vthunder_orchestrator +namespace a10vthunder { public class AnyErrors { diff --git a/a10vthunder-orchestrator/Api/Models/AuthRequest.cs b/a10vthunder-orchestrator/Api/Models/AuthRequest.cs index e26a38a..efa45ba 100644 --- a/a10vthunder-orchestrator/Api/Models/AuthRequest.cs +++ b/a10vthunder-orchestrator/Api/Models/AuthRequest.cs @@ -1,4 +1,18 @@ -namespace a10vthunder_orchestrator.Api.Models +// 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. + +namespace a10vthunder.Api.Models { #region JSON Request and Response Classes diff --git a/a10vthunder-orchestrator/Api/Models/AuthResponse.cs b/a10vthunder-orchestrator/Api/Models/AuthResponse.cs index aae18ad..52ae028 100644 --- a/a10vthunder-orchestrator/Api/Models/AuthResponse.cs +++ b/a10vthunder-orchestrator/Api/Models/AuthResponse.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class AuthResponse { diff --git a/a10vthunder-orchestrator/Api/Models/AuthSignatureResponse.cs b/a10vthunder-orchestrator/Api/Models/AuthSignatureResponse.cs index fba6575..383f22d 100644 --- a/a10vthunder-orchestrator/Api/Models/AuthSignatureResponse.cs +++ b/a10vthunder-orchestrator/Api/Models/AuthSignatureResponse.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class AuthSignatureResponse { diff --git a/a10vthunder-orchestrator/Api/Models/ClientTemplateListResponse.cs b/a10vthunder-orchestrator/Api/Models/ClientTemplateListResponse.cs new file mode 100644 index 0000000..2bf1ba5 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/ClientTemplateListResponse.cs @@ -0,0 +1,574 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace a10vthunder.Api.Models +{ + public class CertificateListItem + { + [JsonProperty("cert")] + public string Cert { get; set; } + + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("uuid")] + public string Uuid { get; set; } + + [JsonProperty("a10-url")] + public string A10Url { get; set; } + } + + public class WebCategory + { + [JsonProperty("uncategorized")] + public int? Uncategorized { get; set; } + + [JsonProperty("real-estate")] + public int? RealEstate { get; set; } + + [JsonProperty("computer-and-internet-security")] + public int? ComputerAndInternetSecurity { get; set; } + + [JsonProperty("financial-services")] + public int? FinancialServices { get; set; } + + [JsonProperty("business-and-economy")] + public int? BusinessAndEconomy { get; set; } + + [JsonProperty("computer-and-internet-info")] + public int? ComputerAndInternetInfo { get; set; } + + [JsonProperty("auctions")] + public int? Auctions { get; set; } + + [JsonProperty("shopping")] + public int? Shopping { get; set; } + + [JsonProperty("cult-and-occult")] + public int? CultAndOccult { get; set; } + + [JsonProperty("travel")] + public int? Travel { get; set; } + + [JsonProperty("drugs")] + public int? Drugs { get; set; } + + [JsonProperty("adult-and-pornography")] + public int? AdultAndPornography { get; set; } + + [JsonProperty("home-and-garden")] + public int? HomeAndGarden { get; set; } + + [JsonProperty("military")] + public int? Military { get; set; } + + [JsonProperty("social-network")] + public int? SocialNetwork { get; set; } + + [JsonProperty("dead-sites")] + public int? DeadSites { get; set; } + + [JsonProperty("stock-advice-and-tools")] + public int? StockAdviceAndTools { get; set; } + + [JsonProperty("training-and-tools")] + public int? TrainingAndTools { get; set; } + + [JsonProperty("dating")] + public int? Dating { get; set; } + + [JsonProperty("sex-education")] + public int? SexEducation { get; set; } + + [JsonProperty("religion")] + public int? Religion { get; set; } + + [JsonProperty("entertainment-and-arts")] + public int? EntertainmentAndArts { get; set; } + + [JsonProperty("personal-sites-and-blogs")] + public int? PersonalSitesAndBlogs { get; set; } + + [JsonProperty("legal")] + public int? Legal { get; set; } + + [JsonProperty("local-information")] + public int? LocalInformation { get; set; } + + [JsonProperty("streaming-media")] + public int? StreamingMedia { get; set; } + + [JsonProperty("job-search")] + public int? JobSearch { get; set; } + + [JsonProperty("gambling")] + public int? Gambling { get; set; } + + [JsonProperty("translation")] + public int? Translation { get; set; } + + [JsonProperty("reference-and-research")] + public int? ReferenceAndResearch { get; set; } + + [JsonProperty("shareware-and-freeware")] + public int? SharewareAndFreeware { get; set; } + + [JsonProperty("peer-to-peer")] + public int? PeerToPeer { get; set; } + + [JsonProperty("marijuana")] + public int? Marijuana { get; set; } + + [JsonProperty("hacking")] + public int? Hacking { get; set; } + + [JsonProperty("games")] + public int? Games { get; set; } + + [JsonProperty("philosophy-and-politics")] + public int? PhilosophyAndPolitics { get; set; } + + [JsonProperty("weapons")] + public int? Weapons { get; set; } + + [JsonProperty("pay-to-surf")] + public int? PayToSurf { get; set; } + + [JsonProperty("hunting-and-fishing")] + public int? HuntingAndFishing { get; set; } + + [JsonProperty("society")] + public int? Society { get; set; } + + [JsonProperty("educational-institutions")] + public int? EducationalInstitutions { get; set; } + + [JsonProperty("online-greeting-cards")] + public int? OnlineGreetingCards { get; set; } + + [JsonProperty("sports")] + public int? Sports { get; set; } + + [JsonProperty("swimsuits-and-intimate-apparel")] + public int? SwimsuitsAndIntimateApparel { get; set; } + + [JsonProperty("questionable")] + public int? Questionable { get; set; } + + [JsonProperty("kids")] + public int? Kids { get; set; } + + [JsonProperty("hate-and-racism")] + public int? HateAndRacism { get; set; } + + [JsonProperty("personal-storage")] + public int? PersonalStorage { get; set; } + + [JsonProperty("violence")] + public int? Violence { get; set; } + + [JsonProperty("keyloggers-and-monitoring")] + public int? KeyloggersAndMonitoring { get; set; } + + [JsonProperty("search-engines")] + public int? SearchEngines { get; set; } + + [JsonProperty("internet-portals")] + public int? InternetPortals { get; set; } + + [JsonProperty("web-advertisements")] + public int? WebAdvertisements { get; set; } + + [JsonProperty("cheating")] + public int? Cheating { get; set; } + + [JsonProperty("gross")] + public int? Gross { get; set; } + + [JsonProperty("web-based-email")] + public int? WebBasedEmail { get; set; } + + [JsonProperty("malware-sites")] + public int? MalwareSites { get; set; } + + [JsonProperty("phishing-and-other-fraud")] + public int? PhishingAndOtherFraud { get; set; } + + [JsonProperty("proxy-avoid-and-anonymizers")] + public int? ProxyAvoidAndAnonymizers { get; set; } + + [JsonProperty("spyware-and-adware")] + public int? SpywareAndAdware { get; set; } + + [JsonProperty("music")] + public int? Music { get; set; } + + [JsonProperty("government")] + public int? Government { get; set; } + + [JsonProperty("nudity")] + public int? Nudity { get; set; } + + [JsonProperty("news-and-media")] + public int? NewsAndMedia { get; set; } + + [JsonProperty("illegal")] + public int? Illegal { get; set; } + + [JsonProperty("cdns")] + public int? Cdns { get; set; } + + [JsonProperty("internet-communications")] + public int? InternetCommunications { get; set; } + + [JsonProperty("bot-nets")] + public int? BotNets { get; set; } + + [JsonProperty("abortion")] + public int? Abortion { get; set; } + + [JsonProperty("health-and-medicine")] + public int? HealthAndMedicine { get; set; } + + [JsonProperty("confirmed-spam-sources")] + public int? ConfirmedSpamSources { get; set; } + + [JsonProperty("spam-urls")] + public int? SpamUrls { get; set; } + + [JsonProperty("unconfirmed-spam-sources")] + public int? UnconfirmedSpamSources { get; set; } + + [JsonProperty("open-http-proxies")] + public int? OpenHttpProxies { get; set; } + + [JsonProperty("dynamic-comment")] + public int? DynamicComment { get; set; } + + [JsonProperty("parked-domains")] + public int? ParkedDomains { get; set; } + + [JsonProperty("alcohol-and-tobacco")] + public int? AlcoholAndTobacco { get; set; } + + [JsonProperty("private-ip-addresses")] + public int? PrivateIpAddresses { get; set; } + + [JsonProperty("image-and-video-search")] + public int? ImageAndVideoSearch { get; set; } + + [JsonProperty("fashion-and-beauty")] + public int? FashionAndBeauty { get; set; } + + [JsonProperty("recreation-and-hobbies")] + public int? RecreationAndHobbies { get; set; } + + [JsonProperty("motor-vehicles")] + public int? MotorVehicles { get; set; } + + [JsonProperty("web-hosting-sites")] + public int? WebHostingSites { get; set; } + + [JsonProperty("food-and-dining")] + public int? FoodAndDining { get; set; } + } + + public class WebReputation + { + [JsonProperty("bypass-trustworthy")] + public int? BypassTrustworthy { get; set; } + } + + public class ExceptionWebReputation + { + [JsonProperty("exception-trustworthy")] + public int? ExceptionTrustworthy { get; set; } + } + + public class ClientSslList + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("auth-username")] + public string AuthUsername { get; set; } + + // v4 direct properties (will be null in v6) + [JsonProperty("cert")] + public string Cert { get; set; } + + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("local-logging")] + public int? LocalLogging { get; set; } + + [JsonProperty("ocsp-stapling")] + public int? OcspStapling { get; set; } + + // v6 specific properties (will be null in v4) + [JsonProperty("require-sni-cert-matched")] + public int? RequireSniCertMatched { get; set; } + + [JsonProperty("ssli-inbound-enable")] + public int? SsliInboundEnable { get; set; } + + [JsonProperty("ssli-logging")] + public int? SsliLogging { get; set; } + + [JsonProperty("client-certificate")] + public string ClientCertificate { get; set; } + + [JsonProperty("close-notify")] + public int? CloseNotify { get; set; } + + [JsonProperty("forward-proxy-alt-sign")] + public int? ForwardProxyAltSign { get; set; } + + [JsonProperty("enable-tls-alert-logging")] + public int? EnableTlsAlertLogging { get; set; } + + [JsonProperty("forward-proxy-verify-cert-fail-action")] + public int? ForwardProxyVerifyCertFailAction { get; set; } + + [JsonProperty("verify-cert-fail-action")] + public string VerifyCertFailAction { get; set; } + + [JsonProperty("forward-proxy-cert-revoke-action")] + public int? ForwardProxyCertRevokeAction { get; set; } + + [JsonProperty("cert-revoke-action")] + public string CertRevokeAction { get; set; } + + [JsonProperty("forward-proxy-no-shared-cipher-action")] + public int? ForwardProxyNoSharedCipherAction { get; set; } + + [JsonProperty("no-shared-cipher-action")] + public string NoSharedCipherAction { get; set; } + + // v6 specific + [JsonProperty("forward-proxy-esni-action")] + public int? ForwardProxyEsniAction { get; set; } + + [JsonProperty("forward-proxy-cert-unknown-action")] + public int? ForwardProxyCertUnknownAction { get; set; } + + [JsonProperty("cert-unknown-action")] + public string CertUnknownAction { get; set; } + + [JsonProperty("notbefore")] + public int? NotBefore { get; set; } + + [JsonProperty("notafter")] + public int? NotAfter { get; set; } + + [JsonProperty("forward-proxy-ssl-version")] + public int? ForwardProxySslVersion { get; set; } + + [JsonProperty("forward-proxy-ocsp-disable")] + public int? ForwardProxyOcspDisable { get; set; } + + [JsonProperty("forward-proxy-crl-disable")] + public int? ForwardProxyCrlDisable { get; set; } + + [JsonProperty("forward-proxy-cert-cache-timeout")] + public int? ForwardProxyCertCacheTimeout { get; set; } + + [JsonProperty("forward-proxy-cert-cache-limit")] + public int? ForwardProxyCertCacheLimit { get; set; } + + [JsonProperty("forward-proxy-cert-expiry")] + public int? ForwardProxyCertExpiry { get; set; } + + // v6 specific - missing from v4 + [JsonProperty("forward-proxy-enable")] + public int? ForwardProxyEnable { get; set; } + + [JsonProperty("handshake-logging-enable")] + public int? HandshakeLoggingEnable { get; set; } + + [JsonProperty("session-key-logging-enable")] + public int? SessionKeyLoggingEnable { get; set; } + + [JsonProperty("forward-proxy-selfsign-redir")] + public int? ForwardProxySelfsignRedir { get; set; } + + [JsonProperty("forward-proxy-failsafe-disable")] + public int? ForwardProxyFailsafeDisable { get; set; } + + [JsonProperty("forward-proxy-log-disable")] + public int? ForwardProxyLogDisable { get; set; } + + [JsonProperty("forward-proxy-no-sni-action")] + public string ForwardProxyNoSniAction { get; set; } + + [JsonProperty("case-insensitive")] + public int? CaseInsensitive { get; set; } + + [JsonProperty("client-auth-case-insensitive")] + public int? ClientAuthCaseInsensitive { get; set; } + + [JsonProperty("forward-proxy-cert-not-ready-action")] + public string ForwardProxyCertNotReadyAction { get; set; } + + // v4 has web-category, v6 has web-reputation objects + [JsonProperty("web-category")] + public WebCategory WebCategory { get; set; } + + [JsonProperty("web-reputation")] + public WebReputation WebReputation { get; set; } + + [JsonProperty("exception-web-reputation")] + public ExceptionWebReputation ExceptionWebReputation { get; set; } + + [JsonProperty("require-web-category")] + public int? RequireWebCategory { get; set; } + + // v6 specific + [JsonProperty("central-cert-pin-list")] + public int? CentralCertPinList { get; set; } + + [JsonProperty("server-name-auto-map")] + public int? ServerNameAutoMap { get; set; } + + [JsonProperty("sni-bypass-missing-cert")] + public int? SniBypassMissingCert { get; set; } + + [JsonProperty("sni-bypass-expired-cert")] + public int? SniBypassExpiredCert { get; set; } + + [JsonProperty("sni-bypass-enable-log")] + public int? SniBypassEnableLog { get; set; } + + [JsonProperty("direct-client-server-auth")] + public int? DirectClientServerAuth { get; set; } + + [JsonProperty("session-cache-size")] + public int? SessionCacheSize { get; set; } + + [JsonProperty("session-cache-timeout")] + public int? SessionCacheTimeout { get; set; } + + [JsonProperty("session-ticket-disable")] + public int? SessionTicketDisable { get; set; } + + [JsonProperty("session-ticket-lifetime")] + public int? SessionTicketLifetime { get; set; } + + [JsonProperty("ssl-false-start-disable")] + public int? SslFalseStartDisable { get; set; } + + [JsonProperty("disable-sslv3")] + public int? DisableSslV3 { get; set; } + + [JsonProperty("version")] + public int? Version { get; set; } + + [JsonProperty("dgversion")] + public int? DgVersion { get; set; } + + [JsonProperty("renegotiation-disable")] + public int? RenegotiationDisable { get; set; } + + [JsonProperty("authorization")] + public int? Authorization { get; set; } + + // v6 specific + [JsonProperty("early-data")] + public int? EarlyData { get; set; } + + [JsonProperty("ja3-enable")] + public int? Ja3Enable { get; set; } + + [JsonProperty("ja4-enable")] + public int? Ja4Enable { get; set; } + + [JsonProperty("uuid")] + public string Uuid { get; set; } + + // v6 has certificate-list, v4 has direct cert/key + [JsonProperty("certificate-list")] + public List CertificateList { get; set; } + + [JsonProperty("a10-url")] + public string A10Url { get; set; } + + // Helper methods to work with both formats + public string GetCertificate() + { + // v4 format - direct cert property + if (!string.IsNullOrEmpty(Cert)) + return Cert; + + // v6 format - certificate-list + if (CertificateList?.Count > 0) + return CertificateList[0].Cert; + + return null; + } + + public string GetKey() + { + // v4 format - direct key property + if (!string.IsNullOrEmpty(Key)) + return Key; + + // v6 format - certificate-list + if (CertificateList?.Count > 0) + return CertificateList[0].Key; + + return null; + } + + public List GetAllCertificates() + { + // v6 format - return certificate list directly + if (CertificateList?.Count > 0) + return CertificateList; + + // v4 format - create a certificate list item from direct properties + if (!string.IsNullOrEmpty(Cert) || !string.IsNullOrEmpty(Key)) + { + return new List + { + new CertificateListItem + { + Cert = Cert, + Key = Key, + Uuid = Uuid, + A10Url = A10Url + } + }; + } + + return new List(); + } + + // Helper method to check if this template uses a specific certificate + public bool UsesCertificate(string certName) + { + if (string.IsNullOrEmpty(certName)) + return false; + + // v4 format - check direct cert property + if (!string.IsNullOrEmpty(Cert) && + string.Equals(Cert, certName, StringComparison.OrdinalIgnoreCase)) + return true; + + // v6 format - check certificate-list + if (CertificateList?.Any(c => + string.Equals(c.Cert, certName, StringComparison.OrdinalIgnoreCase)) == true) + return true; + + return false; + } + } + + public class ClientTemplateListResponse + { + [JsonProperty("client-ssl-list")] + public List ClientSslList { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/Credentials.cs b/a10vthunder-orchestrator/Api/Models/Credentials.cs index 08d4b57..76533ff 100644 --- a/a10vthunder-orchestrator/Api/Models/Credentials.cs +++ b/a10vthunder-orchestrator/Api/Models/Credentials.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { internal class Credentials { diff --git a/a10vthunder-orchestrator/Api/Models/DeleteCertBaseRequest.cs b/a10vthunder-orchestrator/Api/Models/DeleteCertBaseRequest.cs index f6771c9..779d280 100644 --- a/a10vthunder-orchestrator/Api/Models/DeleteCertBaseRequest.cs +++ b/a10vthunder-orchestrator/Api/Models/DeleteCertBaseRequest.cs @@ -1,11 +1,22 @@ -using Newtonsoft.Json; - -namespace a10vthunder_orchestrator.Api.Models +// 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 Newtonsoft.Json; +namespace a10vthunder.Api.Models { public class DeleteCertBaseRequest { - [JsonProperty("delete", NullValueHandling = NullValueHandling.Ignore)] - public DeleteCertRequest DeleteCert { get; set; } - + [JsonProperty("delete")] + public DeleteCertRequest Delete { get; set; } } -} +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/DeleteCertRequest.cs b/a10vthunder-orchestrator/Api/Models/DeleteCertRequest.cs index d5c2b90..6b3d123 100644 --- a/a10vthunder-orchestrator/Api/Models/DeleteCertRequest.cs +++ b/a10vthunder-orchestrator/Api/Models/DeleteCertRequest.cs @@ -1,13 +1,25 @@ -using Newtonsoft.Json; - -namespace a10vthunder_orchestrator.Api.Models +// 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 Newtonsoft.Json; +namespace a10vthunder.Api.Models { public class DeleteCertRequest { - [JsonProperty("cert-name")] - public string CertName { get; set; } - - [JsonProperty("private-key",NullValueHandling=NullValueHandling.Ignore)] + [JsonProperty("cert-name", NullValueHandling = NullValueHandling.Ignore)] + public string CertName { get; set; } + + [JsonProperty("private-key", NullValueHandling = NullValueHandling.Ignore)] public string PrivateKey { get; set; } } -} +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/ManagementErrorResponse.cs b/a10vthunder-orchestrator/Api/Models/ManagementErrorResponse.cs new file mode 100644 index 0000000..021c6ba --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/ManagementErrorResponse.cs @@ -0,0 +1,39 @@ +// 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 Newtonsoft.Json; + +namespace a10vthunder.Api.Models +{ + public class Err + { + public int code { get; set; } + public string msg { get; set; } + public string location { get; set; } + } + + public class ErrorResponse + { + [JsonProperty("http-status")] + public int httpstatus { get; set; } + public string status { get; set; } + public Err err { get; set; } + } + + public class ManagementErrorResponse + { + public Response response { get; set; } + } + +} diff --git a/a10vthunder-orchestrator/Api/Models/ManagementSuccessResponse.cs b/a10vthunder-orchestrator/Api/Models/ManagementSuccessResponse.cs new file mode 100644 index 0000000..336563f --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/ManagementSuccessResponse.cs @@ -0,0 +1,31 @@ +// 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 Newtonsoft.Json; + +namespace a10vthunder.Api.Models +{ + public class Response + { + [JsonProperty("http-status")] + public int httpstatus { get; set; } + public string status { get; set; } + public string msg { get; set; } + } + + public class ManagementSuccessResponse + { + public Response response { get; set; } + } +} diff --git a/a10vthunder-orchestrator/Api/Models/Operation.cs b/a10vthunder-orchestrator/Api/Models/Operation.cs index 047f07c..fe5c9b3 100644 --- a/a10vthunder-orchestrator/Api/Models/Operation.cs +++ b/a10vthunder-orchestrator/Api/Models/Operation.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class Operation { diff --git a/a10vthunder-orchestrator/Api/Models/ServerTemplateListResponse.cs b/a10vthunder-orchestrator/Api/Models/ServerTemplateListResponse.cs new file mode 100644 index 0000000..7b34a4e --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/ServerTemplateListResponse.cs @@ -0,0 +1,162 @@ +// Copyright 2025 Keyfactor +// SPDX-License-Identifier: Apache-2.0 + +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace a10vthunder.Api.Models +{ + public class ServerCertificate + { + [JsonProperty("cert")] + public string Cert { get; set; } + + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("uuid")] + public string Uuid { get; set; } + + [JsonProperty("a10-url")] + public string A10Url { get; set; } + } + + public class ServerSslList + { + [JsonProperty("name")] + public string Name { get; set; } + + // v4 direct properties (will be null in v6) + [JsonProperty("cert")] + public string Cert { get; set; } + + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("enable-tls-alert-logging")] + public int? EnableTlsAlertLogging { get; set; } + + [JsonProperty("handshake-logging-enable")] + public int? HandshakeLoggingEnable { get; set; } + + // v6 specific property (will be null in v4) + [JsonProperty("session-key-logging-enable")] + public int? SessionKeyLoggingEnable { get; set; } + + [JsonProperty("close-notify")] + public int? CloseNotify { get; set; } + + [JsonProperty("forward-proxy-enable")] + public int? ForwardProxyEnable { get; set; } + + [JsonProperty("session-ticket-enable")] + public int? SessionTicketEnable { get; set; } + + [JsonProperty("version")] + public int? Version { get; set; } + + [JsonProperty("dgversion")] + public int? DgVersion { get; set; } + + [JsonProperty("ssli-logging")] + public int? SsliLogging { get; set; } + + // v4 specific property (will be null in v6) + [JsonProperty("dh-short-key-action")] + public string DhShortKeyAction { get; set; } + + [JsonProperty("ocsp-stapling")] + public int? OcspStapling { get; set; } + + [JsonProperty("use-client-sni")] + public int? UseClientSni { get; set; } + + [JsonProperty("renegotiation-disable")] + public int? RenegotiationDisable { get; set; } + + [JsonProperty("session-cache-size")] + public int? SessionCacheSize { get; set; } + + // v6 specific property (will be null in v4) + [JsonProperty("early-data")] + public int? EarlyData { get; set; } + + [JsonProperty("uuid")] + public string Uuid { get; set; } + + // v6 has certificate object, v4 has direct cert/key + [JsonProperty("certificate")] + public ServerCertificate Certificate { get; set; } + + [JsonProperty("a10-url")] + public string A10Url { get; set; } + + // Helper methods to work with both formats (consistent with client model) + public string GetCertificate() + { + // v4 format - direct cert property + if (!string.IsNullOrEmpty(Cert)) + return Cert; + + // v6 format - certificate object + if (Certificate != null && !string.IsNullOrEmpty(Certificate.Cert)) + return Certificate.Cert; + + return null; + } + + public string GetKey() + { + // v4 format - direct key property + if (!string.IsNullOrEmpty(Key)) + return Key; + + // v6 format - certificate object + if (Certificate != null && !string.IsNullOrEmpty(Certificate.Key)) + return Certificate.Key; + + return null; + } + + public ServerCertificate GetCertificateObject() + { + // v6 format - return certificate object directly + if (Certificate != null) + return Certificate; + + // v4 format - create certificate object from direct properties + if (!string.IsNullOrEmpty(Cert) || !string.IsNullOrEmpty(Key)) + { + return new ServerCertificate + { + Cert = Cert, + Key = Key, + Uuid = Uuid, + A10Url = A10Url?.EndsWith("/certificate") == true + ? A10Url + : A10Url + "/certificate" + }; + } + + return null; + } + + // Check if this is v4 format + public bool IsV4Format() + { + return !string.IsNullOrEmpty(Cert) || !string.IsNullOrEmpty(Key) || !string.IsNullOrEmpty(DhShortKeyAction); + } + + // Check if this is v6 format + public bool IsV6Format() + { + return Certificate != null || SessionKeyLoggingEnable.HasValue || EarlyData.HasValue; + } + } + + public class ServerTemplateListResponse + { + [JsonProperty("server-ssl-list")] + public List ServerSslList { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/SetPartitionRequest.cs b/a10vthunder-orchestrator/Api/Models/SetPartitionRequest.cs new file mode 100644 index 0000000..c07216d --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/SetPartitionRequest.cs @@ -0,0 +1,29 @@ +// 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 Newtonsoft.Json; + +namespace a10vthunder.Api.Models +{ + public class ActivePartition + { + public string curr_part_name { get; set; } + } + + public class SetPartitionRequest + { + [JsonProperty("active-partition")] + public ActivePartition activepartition { get; set; } + } +} diff --git a/a10vthunder-orchestrator/Api/Models/SslCert.cs b/a10vthunder-orchestrator/Api/Models/SslCert.cs index 5e5daf0..2d9c46a 100644 --- a/a10vthunder-orchestrator/Api/Models/SslCert.cs +++ b/a10vthunder-orchestrator/Api/Models/SslCert.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class SslCert { diff --git a/a10vthunder-orchestrator/Api/Models/SslCertKey.cs b/a10vthunder-orchestrator/Api/Models/SslCertKey.cs index 5168f7f..6e62356 100644 --- a/a10vthunder-orchestrator/Api/Models/SslCertKey.cs +++ b/a10vthunder-orchestrator/Api/Models/SslCertKey.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class SslCertKey { diff --git a/a10vthunder-orchestrator/Api/Models/SslCertificate.cs b/a10vthunder-orchestrator/Api/Models/SslCertificate.cs index c4067b5..75c35fa 100644 --- a/a10vthunder-orchestrator/Api/Models/SslCertificate.cs +++ b/a10vthunder-orchestrator/Api/Models/SslCertificate.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; - -namespace a10vthunder_orchestrator.Api.Models +// 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 Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class SslCertificate { diff --git a/a10vthunder-orchestrator/Api/Models/SslCertificateCollection.cs b/a10vthunder-orchestrator/Api/Models/SslCertificateCollection.cs index 7f00806..3bfcbbc 100644 --- a/a10vthunder-orchestrator/Api/Models/SslCertificateCollection.cs +++ b/a10vthunder-orchestrator/Api/Models/SslCertificateCollection.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class SslCertificateCollection { diff --git a/a10vthunder-orchestrator/Api/Models/SslCertificateRequest.cs b/a10vthunder-orchestrator/Api/Models/SslCertificateRequest.cs index 9e47ab5..fbab703 100644 --- a/a10vthunder-orchestrator/Api/Models/SslCertificateRequest.cs +++ b/a10vthunder-orchestrator/Api/Models/SslCertificateRequest.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class SslCertificateRequest { diff --git a/a10vthunder-orchestrator/Api/Models/SslCollectionResponse.cs b/a10vthunder-orchestrator/Api/Models/SslCollectionResponse.cs index bcd7fda..2321915 100644 --- a/a10vthunder-orchestrator/Api/Models/SslCollectionResponse.cs +++ b/a10vthunder-orchestrator/Api/Models/SslCollectionResponse.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class SslCollectionResponse { diff --git a/a10vthunder-orchestrator/Api/Models/SslKeyRequest.cs b/a10vthunder-orchestrator/Api/Models/SslKeyRequest.cs index 996e415..40e2d56 100644 --- a/a10vthunder-orchestrator/Api/Models/SslKeyRequest.cs +++ b/a10vthunder-orchestrator/Api/Models/SslKeyRequest.cs @@ -1,6 +1,20 @@ -using Newtonsoft.Json; +// 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. -namespace a10vthunder_orchestrator.Api.Models +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models { public class SslKeyRequest { diff --git a/a10vthunder-orchestrator/Api/Models/UpdateClientTemplateResponse.cs b/a10vthunder-orchestrator/Api/Models/UpdateClientTemplateResponse.cs new file mode 100644 index 0000000..618052a --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/UpdateClientTemplateResponse.cs @@ -0,0 +1,36 @@ +// 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 Newtonsoft.Json; +using System.Collections.Generic; + +namespace a10vthunder.Api.Models +{ + public class CleintCertificateList + { + public string cert { get; set; } + public string key { get; set; } + public string uuid { get; set; } + + [JsonProperty("a10-url")] + public string a10url { get; set; } + } + + public class UpdateClientTemplateResponse + { + [JsonProperty("certificate-list")] + public List certificatelist { get; set; } + } + +} diff --git a/a10vthunder-orchestrator/Api/Models/UpdateServerTemplateResponse.cs b/a10vthunder-orchestrator/Api/Models/UpdateServerTemplateResponse.cs new file mode 100644 index 0000000..6df16ae --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/UpdateServerTemplateResponse.cs @@ -0,0 +1,33 @@ +// 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 Newtonsoft.Json; + +namespace a10vthunder.Api.Models +{ + public class UpdateTemplateResposneCertificate + { + public string cert { get; set; } + public string key { get; set; } + public string uuid { get; set; } + + [JsonProperty("a10-url")] + public string a10url { get; set; } + } + + public class UpdateServerTemplateResponse + { + public UpdateTemplateResposneCertificate certificate { get; set; } + } +} diff --git a/a10vthunder-orchestrator/Api/Models/UpdateTemplateRequest.cs b/a10vthunder-orchestrator/Api/Models/UpdateTemplateRequest.cs new file mode 100644 index 0000000..15c84e3 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/UpdateTemplateRequest.cs @@ -0,0 +1,22 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 + +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models +{ + public class UpdateTemplateCertificate + { + [JsonProperty("cert")] + public string Cert { get; set; } + + [JsonProperty("key")] + public string Key { get; set; } + } + + public class UpdateTemplateRequest + { + [JsonProperty("certificate")] + public UpdateTemplateCertificate Certificate { get; set; } + } +} diff --git a/a10vthunder-orchestrator/Api/Models/VersionResponse.cs b/a10vthunder-orchestrator/Api/Models/VersionResponse.cs new file mode 100644 index 0000000..241c942 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/VersionResponse.cs @@ -0,0 +1,109 @@ +using Newtonsoft.Json; + +namespace a10vthunder.Api.Models +{ + public class VersionResponse + { + [JsonProperty("version")] + public VersionContainer Version { get; set; } + } + + public class VersionContainer + { + [JsonProperty("oper")] + public VersionOperational Oper { get; set; } + + [JsonProperty("a10-url")] + public string A10Url { get; set; } + } + + public class VersionOperational + { + [JsonProperty("hw-platform")] + public string HwPlatform { get; set; } + + [JsonProperty("copyright")] + public string Copyright { get; set; } + + [JsonProperty("sw-version")] + public string SwVersion { get; set; } + + [JsonProperty("plat-features")] + public string PlatFeatures { get; set; } + + [JsonProperty("boot-from")] + public string BootFrom { get; set; } + + [JsonProperty("serial-number")] + public string SerialNumber { get; set; } + + [JsonProperty("aflex-version")] + public string AflexVersion { get; set; } + + [JsonProperty("axapi-version")] + public string AxapiVersion { get; set; } + + [JsonProperty("pri-gui-version")] + public string PriGuiVersion { get; set; } + + [JsonProperty("sec-gui-version")] + public string SecGuiVersion { get; set; } + + [JsonProperty("cylance-version")] + public string CylanceVersion { get; set; } + + [JsonProperty("hd-pri")] + public string HdPri { get; set; } + + [JsonProperty("hd-sec")] + public string HdSec { get; set; } + + [JsonProperty("last-config-saved-time")] + public string LastConfigSavedTime { get; set; } + + [JsonProperty("virtualization-type")] + public string VirtualizationType { get; set; } + + [JsonProperty("sys-poll-mode")] + public string SysPollMode { get; set; } + + [JsonProperty("product")] + public string Product { get; set; } + + [JsonProperty("hw-code")] + public string HwCode { get; set; } + + [JsonProperty("current-time")] + public string CurrentTime { get; set; } + + [JsonProperty("up-time")] + public string UpTime { get; set; } + + [JsonProperty("nun-ctrl-cpus")] + public int NunCtrlCpus { get; set; } + + [JsonProperty("buff-size")] + public int BuffSize { get; set; } + + [JsonProperty("io-buff-enabled")] + public string IoBuffEnabled { get; set; } + + [JsonProperty("build-type")] + public string BuildType { get; set; } + + [JsonProperty("cots-sys-mfg")] + public string CotsSysMfg { get; set; } + + [JsonProperty("cots-sys-name")] + public string CotsSysName { get; set; } + + [JsonProperty("cots-sys-ver")] + public string CotsSysVer { get; set; } + + [JsonProperty("series-name")] + public string SeriesName { get; set; } + + [JsonProperty("hostname")] + public string Hostname { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/Api/Models/VirtualServer.cs b/a10vthunder-orchestrator/Api/Models/VirtualServer.cs new file mode 100644 index 0000000..88bce26 --- /dev/null +++ b/a10vthunder-orchestrator/Api/Models/VirtualServer.cs @@ -0,0 +1,309 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace a10vthunder.Api.Models +{ + public class VirtualServerListResponse + { + [JsonProperty("virtual-server-list")] + public List VirtualServerList { get; set; } + } + + public class VirtualServer + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("ip-address")] + public string IpAddress { get; set; } + + [JsonProperty("enable-disable-action")] + public string EnableDisableAction { get; set; } + + [JsonProperty("redistribution-flagged")] + public int? RedistributionFlagged { get; set; } + + [JsonProperty("arp-disable")] + public int? ArpDisable { get; set; } + + [JsonProperty("stats-data-action")] + public string StatsDataAction { get; set; } + + [JsonProperty("extended-stats")] + public int? ExtendedStats { get; set; } + + [JsonProperty("disable-vip-adv")] + public int? DisableVipAdv { get; set; } + + [JsonProperty("uuid")] + public string Uuid { get; set; } + + [JsonProperty("port-list")] + public List PortList { get; set; } + + [JsonProperty("a10-url")] + public string A10Url { get; set; } + + // Helper methods + public List GetPortsUsingClientTemplate(string templateName) + { + if (string.IsNullOrEmpty(templateName) || PortList == null) + return new List(); + + return PortList.Where(p => + string.Equals(p.TemplateClientSsl, templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + public List GetPortsUsingServerTemplate(string templateName) + { + if (string.IsNullOrEmpty(templateName) || PortList == null) + return new List(); + + return PortList.Where(p => + string.Equals(p.TemplateServerSsl, templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + } + + public class VirtualServerPort + { + [JsonProperty("port-number")] + public int? PortNumber { get; set; } + + [JsonProperty("protocol")] + public string Protocol { get; set; } + + [JsonProperty("range")] + public int? Range { get; set; } + + // v6 specific property + [JsonProperty("support-http2")] + public int? SupportHttp2 { get; set; } + + // v4 has this more commonly, v6 may not always have it + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("conn-limit")] + public int? ConnLimit { get; set; } + + [JsonProperty("reset")] + public int? Reset { get; set; } + + [JsonProperty("no-logging")] + public int? NoLogging { get; set; } + + [JsonProperty("action")] + public string Action { get; set; } + + [JsonProperty("def-selection-if-pref-failed")] + public string DefSelectionIfPrefFailed { get; set; } + + [JsonProperty("skip-rev-hash")] + public int? SkipRevHash { get; set; } + + [JsonProperty("message-switching")] + public int? MessageSwitching { get; set; } + + [JsonProperty("force-routing-mode")] + public int? ForceRoutingMode { get; set; } + + [JsonProperty("reset-on-server-selection-fail")] + public int? ResetOnServerSelectionFail { get; set; } + + [JsonProperty("clientip-sticky-nat")] + public int? ClientipStickyNat { get; set; } + + [JsonProperty("extended-stats")] + public int? ExtendedStats { get; set; } + + [JsonProperty("snat-on-vip")] + public int? SnatOnVip { get; set; } + + [JsonProperty("stats-data-action")] + public string StatsDataAction { get; set; } + + [JsonProperty("syn-cookie")] + public int? SynCookie { get; set; } + + // v6 specific properties + [JsonProperty("showtech-print-extended-stats")] + public int? ShowtechPrintExtendedStats { get; set; } + + [JsonProperty("attack-detection")] + public int? AttackDetection { get; set; } + + [JsonProperty("no-auto-up-on-aflex")] + public int? NoAutoUpOnAflex { get; set; } + + // v4 specific property + [JsonProperty("scaleout-bucket-count")] + public int? ScaleoutBucketCount { get; set; } + + [JsonProperty("auto")] + public int? Auto { get; set; } + + // v6 specific property + [JsonProperty("use-cgnv6")] + public int? UseCgnv6 { get; set; } + + [JsonProperty("ipinip")] + public int? Ipinip { get; set; } + + [JsonProperty("rtp-sip-call-id-match")] + public int? RtpSipCallIdMatch { get; set; } + + [JsonProperty("use-rcv-hop-for-resp")] + public int? UseRcvHopForResp { get; set; } + + [JsonProperty("use-rcv-hop-group")] + public int? UseRcvHopGroup { get; set; } + + // v6 specific property + [JsonProperty("redirect-to-https")] + public int? RedirectToHttps { get; set; } + + [JsonProperty("template-server-ssl")] + public string TemplateServerSsl { get; set; } + + [JsonProperty("template-client-ssl")] + public string TemplateClientSsl { get; set; } + + [JsonProperty("template-virtual-port")] + public string TemplateVirtualPort { get; set; } + + [JsonProperty("use-default-if-no-server")] + public int? UseDefaultIfNoServer { get; set; } + + [JsonProperty("no-dest-nat")] + public int? NoDestNat { get; set; } + + [JsonProperty("cpu-compute")] + public int? CpuCompute { get; set; } + + [JsonProperty("memory-compute")] + public int? MemoryCompute { get; set; } + + [JsonProperty("substitute-source-mac")] + public int? SubstituteSourceMac { get; set; } + + // v6 specific properties + [JsonProperty("aflex-table-entry-syn-disable")] + public int? AflexTableEntrySynDisable { get; set; } + + [JsonProperty("gtp-session-lb")] + public int? GtpSessionLb { get; set; } + + [JsonProperty("reply-acme-challenge")] + public int? ReplyAcmeChallenge { get; set; } + + [JsonProperty("uuid")] + public string Uuid { get; set; } + + [JsonProperty("a10-url")] + public string A10Url { get; set; } + + [JsonProperty("service-group")] + public string ServiceGroup { get; set; } + + // Helper methods + public bool UsesClientTemplate(string templateName) + { + return !string.IsNullOrEmpty(TemplateClientSsl) && + string.Equals(TemplateClientSsl, templateName, StringComparison.OrdinalIgnoreCase); + } + + public bool UsesServerTemplate(string templateName) + { + return !string.IsNullOrEmpty(TemplateServerSsl) && + string.Equals(TemplateServerSsl, templateName, StringComparison.OrdinalIgnoreCase); + } + + public string GetPortIdentifier() + { + return $"{PortNumber}+{Protocol}"; + } + + // Check if this is v4 format + public bool IsV4Format() + { + return ScaleoutBucketCount.HasValue || + (!SupportHttp2.HasValue && !ShowtechPrintExtendedStats.HasValue); + } + + // Check if this is v6 format + public bool IsV6Format() + { + return SupportHttp2.HasValue || ShowtechPrintExtendedStats.HasValue || + AttackDetection.HasValue || UseCgnv6.HasValue || RedirectToHttps.HasValue || + AflexTableEntrySynDisable.HasValue || GtpSessionLb.HasValue; + } + } + + public class VirtualServerPortUpdateRequest + { + [JsonProperty("port")] + public VirtualServerPortUpdate Port { get; set; } + } + + public class VirtualServerPortUpdate + { + [JsonProperty("port-number")] + public int? PortNumber { get; set; } + + [JsonProperty("protocol")] + public string Protocol { get; set; } + + [JsonProperty("template-server-ssl")] + public string TemplateServerSsl { get; set; } + + [JsonProperty("template-client-ssl")] + public string TemplateClientSsl { get; set; } + } + + // Management certificate operations classes + public class ManagementCertRequest + { + [JsonProperty("certificate")] + public ManagementCertificate Certificate { get; set; } + } + + public class ManagementCertificate + { + [JsonProperty("load")] + public int? Load { get; set; } + + [JsonProperty("file-url")] + public string FileUrl { get; set; } + } + + public class ManagementPrivateKeyRequest + { + [JsonProperty("private-key")] + public PrivateKey PrivateKey { get; set; } + } + + public class PrivateKey + { + [JsonProperty("load")] + public int? Load { get; set; } + + [JsonProperty("file-url")] + public string FileUrl { get; set; } + } + + public class ManagementCertRestartRequest + { + [JsonProperty("secure")] + public Secure Secure { get; set; } + } + + public class Secure + { + [JsonProperty("restart")] + public int? Restart { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/CertManager.cs b/a10vthunder-orchestrator/CertManager.cs index a09fbd5..2b593f0 100644 --- a/a10vthunder-orchestrator/CertManager.cs +++ b/a10vthunder-orchestrator/CertManager.cs @@ -1,15 +1,29 @@ -using System; +// 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; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; -using a10vthunder_orchestrator.Api; -using a10vthunder_orchestrator.Api.Models; +using a10vthunder.Api; +using a10vthunder.Api.Models; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; -namespace a10vthunder_orchestrator +namespace a10vthunder { public class CertManager { @@ -71,7 +85,7 @@ public virtual InventoryResult InventoryResult(ApiClient apiClient, string certN if (CertificateCollection != null) foreach (var cc in CertificateCollection.SslCertificate.Oper.SslCertificates) - if (!string.IsNullOrEmpty(cc.Name)) + if (!string.IsNullOrEmpty(cc.Name) && cc.Type.ToLower()!="key") { logger.LogTrace($"Looping through Certificate Store files: {cc.Name}"); diff --git a/a10vthunder-orchestrator/Constants.cs b/a10vthunder-orchestrator/Constants.cs index ab64a03..3b9b559 100644 --- a/a10vthunder-orchestrator/Constants.cs +++ b/a10vthunder-orchestrator/Constants.cs @@ -1,4 +1,18 @@ -namespace a10vthunder_orchestrator +// 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. + +namespace a10vthunder { static class WindowsUserAnyAgentConstants { diff --git a/a10vthunder-orchestrator/Exceptions/InvalidInventoryInvokeException.cs b/a10vthunder-orchestrator/Exceptions/InvalidInventoryInvokeException.cs index d393678..cbb129c 100644 --- a/a10vthunder-orchestrator/Exceptions/InvalidInventoryInvokeException.cs +++ b/a10vthunder-orchestrator/Exceptions/InvalidInventoryInvokeException.cs @@ -1,6 +1,20 @@ -using System; +// 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. -namespace a10vthunder_orchestrator.Exceptions +using System; + +namespace a10vthunder.Exceptions { internal class InvalidInventoryInvokeException : Exception { diff --git a/a10vthunder-orchestrator/Exceptions/UnsupportedOperationException.cs b/a10vthunder-orchestrator/Exceptions/UnsupportedOperationException.cs index c80eb1d..2a526cb 100644 --- a/a10vthunder-orchestrator/Exceptions/UnsupportedOperationException.cs +++ b/a10vthunder-orchestrator/Exceptions/UnsupportedOperationException.cs @@ -1,6 +1,20 @@ -using System; +// 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. -namespace a10vthunder_orchestrator.Exceptions +using System; + +namespace a10vthunder.Exceptions { internal class UnSupportedOperationException : Exception { diff --git a/a10vthunder-orchestrator/ImplementedStoreTypes/Mgmt/Inventory.cs b/a10vthunder-orchestrator/ImplementedStoreTypes/Mgmt/Inventory.cs new file mode 100644 index 0000000..496a328 --- /dev/null +++ b/a10vthunder-orchestrator/ImplementedStoreTypes/Mgmt/Inventory.cs @@ -0,0 +1,157 @@ +// 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; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +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 Renci.SshNet; + +namespace Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderMgmt +{ + public class Inventory : IInventoryJobExtension + { + private ILogger _logger; + + private readonly IPAMSecretResolver _resolver; + + public Inventory(IPAMSecretResolver resolver) + { + _resolver = resolver; + } + + public string ExtensionName => String.Empty; + + public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate submitInventory) + { + _logger = LogHandler.GetClassLogger(); + _logger.MethodEntry(); + + try + { + dynamic props = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); + + string host = props.OrchToScpServerIp; + string username = props.ScpUserName; + string password = props.ScpPassword; + string path = config.CertificateStoreDetails.StorePath; + int port = props.port != null ? (int)props.port : 22; + + _logger.LogInformation($"Connecting to {host} via SSH to list files in {path}"); + + List inventory = new List(); + + using (var sshClient = new SshClient(host, port, username, password)) + using (var scpClient = new ScpClient(host, port, username, password)) + { + sshClient.Connect(); + scpClient.Connect(); + + // List all files in directory + var cmd = sshClient.RunCommand($"ls -1 \"{path}\""); + if (cmd.ExitStatus != 0) + { + throw new Exception($"Failed to list directory: {cmd.Error}"); + } + + var files = cmd.Result.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + foreach (var file in files) + { + string fullFilePath = $"{path}/{file}"; + _logger.LogInformation($"Checking file: {fullFilePath}"); + + using (var ms = new MemoryStream()) + { + try + { + scpClient.Download(fullFilePath, ms); + string pemContent = System.Text.Encoding.UTF8.GetString(ms.ToArray()); + + string cert = ExtractPemBlock(pemContent, "CERTIFICATE"); + + if (!string.IsNullOrEmpty(cert)) + { + string alias = Path.GetFileNameWithoutExtension(file); + + var inventoryItem = new CurrentInventoryItem + { + ItemStatus = OrchestratorInventoryItemStatus.Unknown, + Alias = alias, + PrivateKeyEntry = true, + UseChainLevel = false, + Certificates = new[] { cert } + }; + + inventory.Add(inventoryItem); + _logger.LogInformation($"Added cert to inventory: {alias}"); + } + else + { + _logger.LogDebug($"Skipped file (no certificate found): {file}"); + } + } + catch (Exception fileEx) + { + _logger.LogWarning($"Failed to process file '{file}': {fileEx.Message}"); + } + } + } + + sshClient.Disconnect(); + scpClient.Disconnect(); + } + + bool result = submitInventory.Invoke(inventory); + + return new JobResult + { + JobHistoryId = config.JobHistoryId, + Result = result ? OrchestratorJobStatusJobResult.Success : OrchestratorJobStatusJobResult.Failure, + FailureMessage = result ? null : "Inventory submission failed." + }; + } + catch (Exception ex) + { + _logger.LogError($"Error during SCP Inventory job: {ex.Message}"); + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = $"Inventory Error: {ex.Message}" + }; + } + finally + { + _logger.MethodExit(); + } + } + + + private string ExtractPemBlock(string pem, string blockType) + { + var match = Regex.Match(pem, + $"-----BEGIN {blockType}-----(.*?)-----END {blockType}-----", + RegexOptions.Singleline); + + return match.Success ? match.Value : null; + } + } +} diff --git a/a10vthunder-orchestrator/ImplementedStoreTypes/Mgmt/Management.cs b/a10vthunder-orchestrator/ImplementedStoreTypes/Mgmt/Management.cs new file mode 100644 index 0000000..0cfffe4 --- /dev/null +++ b/a10vthunder-orchestrator/ImplementedStoreTypes/Mgmt/Management.cs @@ -0,0 +1,376 @@ +// 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; +using System.IO; +using System.Text; +using System.Linq; +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.OpenSsl; +using Renci.SshNet; +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Renci.SshNet.Common; +using System.Net.Sockets; +using a10vthunder.Api; + +namespace Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderMgmt +{ + public class Management : IManagementJobExtension + { + private ILogger _logger; + + private readonly IPAMSecretResolver _resolver; + + public Management(IPAMSecretResolver resolver) + { + _resolver = resolver; + } + + public string ExtensionName => String.Empty; + private string ServerPassword { get; set; } + private string ServerUserName { get; set; } + private string VersionInfo { get; set; } + + public string ResolvePamField(string name, string value) + { + _logger.LogTrace($"Attempting to resolved PAM eligible field {name}"); + return _resolver.Resolve(value); + } + + private string GetA10Version(ManagementJobConfiguration config, dynamic props) + { + try + { + _logger.MethodEntry(); + + ServerPassword = ResolvePamField("ServerPassword", config.ServerPassword); + ServerUserName = ResolvePamField("ServerUserName", config.ServerUsername); + + dynamic properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); + var Protocol = properties?.protocol == null || string.IsNullOrEmpty(properties.protocol.Value) + ? "https" + : properties.protocol.Value; + var AllowInvalidCert = + properties?.allowInvalidCert == null || string.IsNullOrEmpty(properties.allowInvalidCert.Value) + ? false + : bool.Parse(properties.allowInvalidCert.Value); + + using (var apiClient = new ApiClient(ServerUserName, ServerPassword, + $"{Protocol}://{config.CertificateStoreDetails.ClientMachine.Trim()}", AllowInvalidCert)) + { + apiClient.Logon(); + var versionResponse = apiClient.GetVersion(); + apiClient.LogOff(); + + var versionInfo = $"Platform: {versionResponse.Version.Oper.HwPlatform}, " + + $"SW Version: {versionResponse.Version.Oper.SwVersion}, " + + $"Hostname: {versionResponse.Version.Oper.Hostname}"; + + _logger.MethodExit(); + return versionInfo; + } + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to get A10 version information: {LogHandler.FlattenException(ex)}"); + return "Version information unavailable"; + } + } + + public JobResult ProcessJob(ManagementJobConfiguration config) + { + _logger = LogHandler.GetClassLogger(); + _logger.MethodEntry(); + + try + { + dynamic props = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); + + // Get version information first + VersionInfo = GetA10Version(config, props); + _logger.LogInformation($"A10 Version Information: {VersionInfo}"); + + string host = props.OrchToScpServerIp; + string username = props.ScpUserName; + string password = props.ScpPassword; + string path = config.CertificateStoreDetails.StorePath; + string filename = config.JobCertificate.Alias; + int port = props.ScpPort != null ? (int)props.ScpPort : 22; + + string fullPath = $"{path}/{filename}.pem"; + + switch (config.OperationType) + { + case CertStoreOperationType.Add: + if (!config.Overwrite && RemoteFileExists(host, port, username, password, fullPath)) + { + return Fail(config, "File already exists. Use Overwrite flag to replace it."); + } + + ReplacePemFiles(config, host, port, username, password, fullPath); + ReplaceCertAndKeyOnA10(config, props, fullPath); + + break; + + case CertStoreOperationType.Remove: + RemovePemFiles(config, host, port, username, password, fullPath); + break; + + default: + return Fail(config, "Unsupported operation. Only Add, Remove, and Replace are supported."); + } + + _logger.MethodExit(); + return new JobResult { JobHistoryId = config.JobHistoryId, Result = OrchestratorJobStatusJobResult.Success }; + } + catch (Exception ex) + { + return Fail(config, $"SSH SCP Error: {LogHandler.FlattenException(ex)}"); + } + } + + private void ReplaceCertAndKeyOnA10(ManagementJobConfiguration config, dynamic props, string fullPath) + { + try + { + _logger.MethodEntry(); + + ServerPassword = ResolvePamField("ServerPassword", config.ServerPassword); + ServerUserName = ResolvePamField("ServerUserName", config.ServerUsername); + + _logger.LogTrace($"config settings: {JsonConvert.SerializeObject(config)}"); + dynamic properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); + _logger.LogTrace($"properties: {JsonConvert.SerializeObject(properties)}"); + var Protocol = properties?.protocol == null || string.IsNullOrEmpty(properties.protocol.Value) + ? "https" + : properties.protocol.Value; + var AllowInvalidCert = + properties?.allowInvalidCert == null || string.IsNullOrEmpty(properties.allowInvalidCert.Value) + ? false + : bool.Parse(properties.allowInvalidCert.Value); + + _logger.LogInformation("Calling A10 API to replace cert and key..."); + + int lastSlash = fullPath.LastIndexOf('/'); + string basePath = fullPath.Substring(0, lastSlash); + string fileOnly = Path.GetFileNameWithoutExtension(fullPath); + + string certPath = $"{basePath}/{fileOnly}.crt"; + string keyPath = $"{basePath}/{fileOnly}.key"; + + string certUrl = $"scp://{properties?.ScpUserName}:{properties?.ScpPassword}@{properties?.A10ToScpServerIp}:{certPath}"; + string keyUrl = $"scp://{properties?.ScpUserName}:{properties?.ScpPassword}@{properties?.A10ToScpServerIp}:{keyPath}"; + + using (var apiClient = new ApiClient(ServerUserName, ServerPassword, + $"{Protocol}://{config.CertificateStoreDetails.ClientMachine.Trim()}", AllowInvalidCert)) + { + apiClient.Logon(); + apiClient.ReplaceCertificateAndKey(certUrl, keyUrl, VersionInfo); + System.Threading.Thread.Sleep(60000); //A10 won't accept new call to write memory right away + apiClient.WriteMemory(); + apiClient.LogOff(); + } + + _logger.LogInformation("A10 certificate and key replacement successful."); + } + catch (Exception ex) + { + throw new ApplicationException($"SCP succeeded but A10 API update failed: {LogHandler.FlattenException(ex)}"); + } + } + + + private void ReplacePemFiles(ManagementJobConfiguration config, string host, int port, string user, string pass, string fullPath) + { + _logger.LogInformation($"Uploading certificate and key to {host}"); + + try + { + var (certPem, keyPem) = GetCertificateAndKey(config); + + int lastSlash = fullPath.LastIndexOf('/'); + string basePath = fullPath.Substring(0, lastSlash); + string filename = Path.GetFileNameWithoutExtension(fullPath); + + string certPath = $"{basePath}/{filename}.crt"; + string keyPath = $"{basePath}/{filename}.key"; + + // Check if files already exist when Overwrite is false + if (!config.Overwrite) + { + if (RemoteFileExists(host, port, user, pass, certPath) || RemoteFileExists(host, port, user, pass, keyPath)) + { + throw new InvalidOperationException("Certificate or key file already exists. Use Overwrite flag to replace them."); + } + } + + var connectionInfo = new PasswordConnectionInfo(host, port, user, pass) + { + Timeout = TimeSpan.FromSeconds(10) + }; + + connectionInfo.AuthenticationBanner += (sender, args) => + { + _logger.LogInformation("SSH Banner: " + args.BannerMessage); + }; + + using (var client = new ScpClient(connectionInfo)) + { + _logger.LogInformation("Connecting to SCP client..."); + client.Connect(); + + if (!client.IsConnected) + { + _logger.LogError("Failed to connect to the SCP server."); + return; + } + + using (var certStream = new MemoryStream(Encoding.UTF8.GetBytes(certPem))) + using (var keyStream = new MemoryStream(Encoding.UTF8.GetBytes(keyPem))) + { + _logger.LogInformation($"Uploading certificate to {certPath}..."); + client.Upload(certStream, certPath); + + _logger.LogInformation($"Uploading private key to {keyPath}..."); + client.Upload(keyStream, keyPath); + } + + client.Disconnect(); + _logger.LogInformation("Upload completed and disconnected."); + } + } + catch (InvalidOperationException invEx) + { + _logger.LogError(invEx.Message); + throw; // Let ProcessJob catch and turn into a JobResult.Failure + } + catch (SshAuthenticationException authEx) + { + _logger.LogError($"Authentication failed: {authEx.Message}"); + } + catch (SshConnectionException connEx) + { + _logger.LogError($"SSH connection error: {connEx.Message}"); + } + catch (SocketException sockEx) + { + _logger.LogError($"Socket error: {sockEx.Message}"); + } + catch (Exception ex) + { + _logger.LogError($"Unexpected error during upload: {ex.Message}"); + } + } + + + private void RemovePemFiles(ManagementJobConfiguration config, string host, int port, string user, string pass, string fullPath) + { + _logger.LogInformation($"Removing certificate and key from {host}"); + + int lastSlash = fullPath.LastIndexOf('/'); + string basePath = fullPath.Substring(0, lastSlash); + string filename = Path.GetFileNameWithoutExtension(fullPath); + + string certPath = $"{basePath}/{filename}.crt"; + string keyPath = $"{basePath}/{filename}.key"; + + using (var ssh = new SshClient(host, port, user, pass)) + { + ssh.Connect(); + + _logger.LogInformation($"Removing cert: {certPath}"); + ssh.RunCommand($"rm -f \"{certPath}\""); + + _logger.LogInformation($"Removing key: {keyPath}"); + ssh.RunCommand($"rm -f \"{keyPath}\""); + + ssh.Disconnect(); + } + + _logger.LogInformation("Certificate and key removed."); + } + + + private bool RemoteFileExists(string host, int port, string user, string pass, string fullPath) + { + using (var ssh = new SshClient(host, port, user, pass)) + { + ssh.Connect(); + var result = ssh.RunCommand($"[ -f \"{fullPath}\" ] && echo \"exists\""); + ssh.Disconnect(); + return result.Result.Trim() == "exists"; + } + } + + private (string certPem, string keyPem) GetCertificateAndKey(ManagementJobConfiguration config) + { + if (!string.IsNullOrEmpty(config.JobCertificate.PrivateKeyPassword)) + { + var certData = Convert.FromBase64String(config.JobCertificate.Contents); + var store = new Pkcs12Store(new MemoryStream(certData), config.JobCertificate.PrivateKeyPassword.ToCharArray()); + + string alias = store.Aliases.Cast().FirstOrDefault(a => store.IsKeyEntry(a)); + var cert = store.GetCertificate(alias).Certificate; + var key = store.GetKey(alias).Key; + + string certPem, keyPem; + + using (var certWriter = new StringWriter()) + { + new PemWriter(certWriter).WriteObject(cert); + certPem = certWriter.ToString(); + } + + using (var keyWriter = new StringWriter()) + { + new PemWriter(keyWriter).WriteObject(key); + keyPem = keyWriter.ToString(); + } + + return (certPem, keyPem); + } + else + { + string certPem = $"-----BEGIN CERTIFICATE-----\n{Pemify(config.JobCertificate.Contents)}\n-----END CERTIFICATE-----"; + string keyPem = ""; // No key provided + return (certPem, keyPem); + } + } + + + private string Pemify(string base64) + { + var sb = new StringBuilder(); + for (int i = 0; i < base64.Length; i += 64) + sb.AppendLine(base64.Substring(i, Math.Min(64, base64.Length - i))); + return sb.ToString().Trim(); + } + + private JobResult Fail(ManagementJobConfiguration config, string message) + { + _logger.LogError(message); + return new JobResult + { + JobHistoryId = config.JobHistoryId, + Result = OrchestratorJobStatusJobResult.Failure, + FailureMessage = message + }; + } + } +} diff --git a/a10vthunder-orchestrator/Jobs/Inventory.cs b/a10vthunder-orchestrator/ImplementedStoreTypes/Ssl/Inventory.cs similarity index 58% rename from a10vthunder-orchestrator/Jobs/Inventory.cs rename to a10vthunder-orchestrator/ImplementedStoreTypes/Ssl/Inventory.cs index d144f8c..dfabfb2 100644 --- a/a10vthunder-orchestrator/Jobs/Inventory.cs +++ b/a10vthunder-orchestrator/ImplementedStoreTypes/Ssl/Inventory.cs @@ -1,20 +1,40 @@ -using System; -using a10vthunder_orchestrator.Api; +// 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; +using a10vthunder; +using a10vthunder.Api; +using a10vthunder.Api.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; -namespace a10vthunder_orchestrator.Jobs + +namespace Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderSsl { public class Inventory : IInventoryJobExtension { - private readonly ILogger _logger; + private ILogger _logger; + + private readonly IPAMSecretResolver _resolver; - public Inventory(ILogger logger) + public Inventory(IPAMSecretResolver resolver) { - _logger = logger; + _resolver = resolver; } protected internal virtual InventoryResult Result { get; set; } @@ -23,11 +43,22 @@ public Inventory(ILogger logger) 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"; + private string ServerPassword { get; set; } + private string ServerUserName { get; set; } + public string ExtensionName => String.Empty; + public string ResolvePamField(string name, string value) + { + _logger.LogTrace($"Attempting to resolved PAM eligible field {name}"); + return _resolver.Resolve(value); + } public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate submitInventory) { + _logger = LogHandler.GetClassLogger(); _logger.MethodEntry(); + + ServerPassword = ResolvePamField("ServerPassword", config.ServerPassword); + ServerUserName = ResolvePamField("ServerUserName", config.ServerUsername); dynamic properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); Protocol = properties != null && @@ -39,10 +70,19 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd ? false : bool.Parse(properties.allowInvalidCert.Value); - using (ApiClient = new ApiClient(config.ServerUsername, config.ServerPassword, + using (ApiClient = new ApiClient(ServerUserName, ServerPassword, $"{Protocol}://{config.CertificateStoreDetails.ClientMachine.Trim()}", AllowInvalidCert)) { ApiClient.Logon(); + ActivePartition activePartition = new ActivePartition + { + curr_part_name = config.CertificateStoreDetails.StorePath + }; + SetPartitionRequest partRequest = new SetPartitionRequest + { + activepartition = activePartition + }; + ApiClient.SetPartition(partRequest); try { _logger.LogTrace("Parse: Certificate Inventory: " + config.CertificateStoreDetails.StorePath); @@ -61,12 +101,12 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd _logger.MethodExit(); if (ReturnValue == false) - return new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId, - FailureMessage = "Error Invoking Inventory" - }; + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = "Error Invoking Inventory" + }; if (Result.Errors.HasError) return new JobResult @@ -78,7 +118,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd }; return new JobResult - {JobHistoryId = config.JobHistoryId, Result = OrchestratorJobStatusJobResult.Success}; + { JobHistoryId = config.JobHistoryId, Result = OrchestratorJobStatusJobResult.Success }; } catch (Exception e) { diff --git a/a10vthunder-orchestrator/ImplementedStoreTypes/Ssl/Management.cs b/a10vthunder-orchestrator/ImplementedStoreTypes/Ssl/Management.cs new file mode 100644 index 0000000..3b1c9a1 --- /dev/null +++ b/a10vthunder-orchestrator/ImplementedStoreTypes/Ssl/Management.cs @@ -0,0 +1,714 @@ +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Collections.Generic; +using a10vthunder; +using a10vthunder.Api; +using a10vthunder.Api.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 Org.BouncyCastle.Crypto; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using static a10vthunder.Api.ApiClient; + +namespace Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderSsl +{ + public class Management : IManagementJobExtension + { + protected internal static Func Pemify = ss => + ss.Length <= 64 ? ss : ss.Substring(0, 64) + "\n" + Pemify(ss.Substring(64)); + + private ILogger _logger; + private readonly IPAMSecretResolver _resolver; + private string VersionInfo { get; set; } + private ApiVersion ApiVersionDetected { get; set; } = ApiVersion.V6; // Default to V6 + + public Management(IPAMSecretResolver resolver) + { + _resolver = resolver; + } + + 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; } + private string ServerPassword { get; set; } + private string ServerUserName { get; set; } + public string ExtensionName => String.Empty; + + public string ResolvePamField(string name, string value) + { + _logger.LogTrace($"Attempting to resolved PAM eligible field {name}"); + return _resolver.Resolve(value); + } + + private ApiVersion GetApiVersion(ManagementJobConfiguration config) + { + try + { + _logger.MethodEntry(); + + ServerPassword = ResolvePamField("ServerPassword", config.ServerPassword); + ServerUserName = ResolvePamField("ServerUserName", config.ServerUsername); + + dynamic properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); + var Protocol = properties?.protocol == null || string.IsNullOrEmpty(properties.protocol.Value) + ? "https" + : properties.protocol.Value; + var AllowInvalidCert = + properties?.allowInvalidCert == null || string.IsNullOrEmpty(properties.allowInvalidCert.Value) + ? false + : bool.Parse(properties.allowInvalidCert.Value); + + using (var apiClient = new ApiClient(ServerUserName, ServerPassword, + $"{Protocol}://{config.CertificateStoreDetails.ClientMachine.Trim()}", AllowInvalidCert)) + { + apiClient.Logon(); + var versionResponse = apiClient.GetVersion(); + apiClient.LogOff(); + + VersionInfo = $"Platform: {versionResponse.Version.Oper.HwPlatform}, " + + $"SW Version: {versionResponse.Version.Oper.SwVersion}, " + + $"Hostname: {versionResponse.Version.Oper.Hostname}"; + + // Determine API version based on software version + // Adjust this logic based on your specific version detection needs + var swVersion = versionResponse.Version.Oper.SwVersion; + + // Example: if version starts with "4." use V4, otherwise use V6 + // Update this logic based on how you differentiate between API versions + if (swVersion != null && swVersion.StartsWith("4.")) + { + _logger.LogInformation($"Detected A10 API Version 4 based on SW Version: {swVersion}"); + return ApiVersion.V4; + } + else + { + _logger.LogInformation($"Using A10 API Version 6 (default) for SW Version: {swVersion}"); + return ApiVersion.V6; + } + } + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to get A10 version information, defaulting to API V6: {LogHandler.FlattenException(ex)}"); + VersionInfo = "Version information unavailable"; + return ApiVersion.V6; // Default to V6 on error + } + finally + { + _logger.MethodExit(); + } + } + + public JobResult ProcessJob(ManagementJobConfiguration config) + { + _logger = LogHandler.GetClassLogger(); + _logger.MethodEntry(); + ServerPassword = ResolvePamField("ServerPassword", config.ServerPassword); + ServerUserName = ResolvePamField("ServerUserName", config.ServerUsername); + + _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); + + // Detect API version first + ApiVersionDetected = GetApiVersion(config); + _logger.LogInformation($"A10 Version Information: {VersionInfo}"); + _logger.LogInformation($"Using API Version: {ApiVersionDetected}"); + + CertManager = new CertManager(); + _logger.LogTrace($"Ending Management Constructor Protocol is {Protocol}"); + + using (ApiClient = new ApiClient(ServerUserName, 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(); + ActivePartition activePartition = new ActivePartition + { + curr_part_name = config.CertificateStoreDetails.StorePath + }; + SetPartitionRequest partRequest = new SetPartitionRequest + { + activepartition = activePartition + }; + ApiClient.SetPartition(partRequest); + InventoryResult = CertManager.GetCert(ApiClient, config.JobCertificate.Alias); + var exactCert = InventoryResult?.InventoryList?.FirstOrDefault(c => c?.Alias?.Equals(config.JobCertificate.Alias, StringComparison.OrdinalIgnoreCase) == true); + + switch (config.OperationType) + { + case CertStoreOperationType.Add: + try + { + if (exactCert != null) + { + 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 (exactCert == null) + { + _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) + { + var virtualServiceBackups = new List(); + + try + { + _logger.LogTrace($"Starting Replace method for {config.JobCertificate.Alias}"); + + // Get server templates using the new model + var assignedServerTemplates = apiClient.GetServerTemplates(); + var serverTemplatesUsingCert = assignedServerTemplates.ServerSslList? + .Where(t => t != null && + (string.Equals(t.GetCertificate(), config.JobCertificate.Alias, StringComparison.OrdinalIgnoreCase) || + string.Equals(t.Name, config.JobCertificate.Alias, StringComparison.OrdinalIgnoreCase))) + .ToList(); + + // Get client templates using the new model + var assignedClientTemplates = apiClient.GetClientTemplates(); + var clientTemplatesUsingCert = assignedClientTemplates?.ClientSslList? + .Where(t => t != null && t.UsesCertificate(config.JobCertificate.Alias)) + .ToList(); + + bool certInUseByServerTemplate = serverTemplatesUsingCert?.Any() == true; + bool certInUseByClientTemplate = clientTemplatesUsingCert?.Any() == true; + + // Get virtual services that use the templates + if (certInUseByServerTemplate || certInUseByClientTemplate) + { + _logger.LogTrace("Certificate is in use by templates. Checking for virtual services using these templates."); + + var virtualServices = apiClient.GetVirtualServices(); + + // Find virtual services using server templates + if (certInUseByServerTemplate) + { + foreach (var template in serverTemplatesUsingCert) + { + // Updated to use VirtualServerList property name + var vsUsingTemplate = virtualServices?.VirtualServerList?.Where(vs => + vs?.PortList?.Any(p => p?.TemplateServerSsl?.Equals(template.Name, StringComparison.OrdinalIgnoreCase) == true) == true).ToList(); + + if (vsUsingTemplate?.Any() == true) + { + foreach (var vs in vsUsingTemplate) + { + var portsUsingTemplate = vs.PortList.Where(p => + p?.TemplateServerSsl?.Equals(template.Name, StringComparison.OrdinalIgnoreCase) == true).ToList(); + + foreach (var port in portsUsingTemplate) + { + virtualServiceBackups.Add(new VirtualServiceBackup + { + VirtualServerName = vs.Name, + Port = port.PortNumber, + Protocol = port.Protocol, + TemplateType = "server-ssl", + TemplateName = template.Name, + OriginalTemplateName = port.TemplateServerSsl + }); + } + } + } + } + } + + // Find virtual services using client templates + if (certInUseByClientTemplate) + { + foreach (var template in clientTemplatesUsingCert) + { + var vsUsingTemplate = virtualServices?.VirtualServerList?.Where(vs => + vs?.PortList?.Any(p => p?.TemplateClientSsl?.Equals(template.Name, StringComparison.OrdinalIgnoreCase) == true) == true).ToList(); + + if (vsUsingTemplate?.Any() == true) + { + foreach (var vs in vsUsingTemplate) + { + var portsUsingTemplate = vs.PortList.Where(p => + p?.TemplateClientSsl?.Equals(template.Name, StringComparison.OrdinalIgnoreCase) == true).ToList(); + + foreach (var port in portsUsingTemplate) + { + virtualServiceBackups.Add(new VirtualServiceBackup + { + VirtualServerName = vs.Name, + Port = port.PortNumber, + Protocol = port.Protocol, + TemplateType = "client-ssl", + TemplateName = template.Name, + OriginalTemplateName = port.TemplateClientSsl + }); + } + } + } + } + } + + // Step 1: Unbind templates from virtual services + if (virtualServiceBackups.Any()) + { + _logger.LogInformation($"Found {virtualServiceBackups.Count} virtual service port bindings to unbind before certificate replacement."); + + foreach (var backup in virtualServiceBackups) + { + _logger.LogTrace($"Unbinding {backup.TemplateType} template '{backup.TemplateName}' from virtual service '{backup.VirtualServerName}' port {backup.Port}"); + apiClient.UnbindTemplateFromVirtualService(backup.VirtualServerName, backup.Port, backup.Protocol, backup.TemplateType); + } + } + } + + string originalAlias = config.JobCertificate.Alias; + + if (certInUseByServerTemplate || certInUseByClientTemplate) + { + _logger.LogInformation($"Certificate {originalAlias} is currently assigned to one or more templates. Preparing to rename and re-import."); + + string newAlias = GenerateNewAlias(originalAlias); + _logger.LogTrace($"Generated new alias: {newAlias}"); + config.JobCertificate.Alias = newAlias; + + // Step 2: Add new cert/key before updating templates + Add(config, apiClient); + + // Step 3: Update all the Server templates using the old cert to now use the new one + if (certInUseByServerTemplate) + { + foreach (var template in serverTemplatesUsingCert) + { + _logger.LogTrace($"Updating Server template {template.Name} to use new cert/key alias {newAlias}"); + + // Use UpdateTemplateRequest for both v4 and v6 formats with version detection + var updateCertRequest = new UpdateTemplateRequest(); + var updateRequest = new UpdateTemplateCertificate + { + Cert = newAlias, + Key = newAlias + }; + updateCertRequest.Certificate = updateRequest; + + // Use the unified method with detected API version + apiClient.UpdateServerTemplates(updateCertRequest, template.Name, ApiVersionDetected); + + _logger.LogInformation($"Server template {template.Name} successfully updated."); + } + } + + // Step 4: Update all the Client templates using the old cert to now use the new one + if (certInUseByClientTemplate) + { + foreach (var template in clientTemplatesUsingCert) + { + _logger.LogTrace($"Updating Client template {template.Name} to use new cert/key alias {newAlias}"); + + // Update the client template to use the new certificate + var updateCertRequest = new UpdateTemplateRequest(); + var updateRequest = new UpdateTemplateCertificate + { + Cert = newAlias, + Key = newAlias + }; + + updateCertRequest.Certificate = updateRequest; + + // Use the unified method with detected API version + apiClient.UpdateClientTemplates(updateCertRequest, template.Name, ApiVersionDetected); + _logger.LogInformation($"Client template {template.Name} successfully updated."); + } + } + + // Step 5: Remove the old certificate + _logger.LogTrace($"Removing old certificate {originalAlias}"); + var tempConfig = new ManagementJobConfiguration + { + JobCertificate = new ManagementJobCertificate { Alias = originalAlias } + }; + Remove(tempConfig, inventoryResult, apiClient); + } + else + { + _logger.LogTrace($"Certificate {originalAlias} is not in use. Proceeding with removal and re-add."); + Remove(config, inventoryResult, apiClient); + Add(config, apiClient); + } + + // Step 6: Re-bind templates to virtual services + if (virtualServiceBackups.Any()) + { + _logger.LogInformation($"Re-binding {virtualServiceBackups.Count} virtual service port bindings after certificate replacement."); + + // 1. Group backups by virtual server + port + protocol + var portBindings = virtualServiceBackups + .GroupBy(b => new { b.VirtualServerName, b.Port, b.Protocol }) + .ToList(); + + // 2. For each unique port, build a single update with both template bindings + foreach (var bindingGroup in portBindings) + { + var vsName = bindingGroup.Key.VirtualServerName; + var port = bindingGroup.Key.Port; + var protocol = bindingGroup.Key.Protocol; + + var update = new VirtualServerPortUpdate + { + PortNumber = port, + Protocol = protocol + }; + + foreach (var b in bindingGroup) + { + if (b.TemplateType.Equals("server-ssl", StringComparison.OrdinalIgnoreCase)) + update.TemplateServerSsl = b.TemplateName; + else if (b.TemplateType.Equals("client-ssl", StringComparison.OrdinalIgnoreCase)) + update.TemplateClientSsl = b.TemplateName; + else + throw new ArgumentException($"Unknown template type: {b.TemplateType}"); + } + + var bindRequest = new VirtualServerPortUpdateRequest { Port = update }; + var requestJson = JsonConvert.SerializeObject(bindRequest); + _logger.LogTrace($"Re-binding templates to VS={vsName} Port={port} Protocol={protocol} => {requestJson}"); + + apiClient.PutVirtualServerPort(vsName, port, protocol, requestJson); + } + } + + apiClient.WriteMemory(); + _logger.LogTrace($"Finished Replace method for {config.JobCertificate.Alias}"); + } + catch (Exception ex) + { + // Rollback: Try to re-bind any unbound virtual services + if (virtualServiceBackups.Any()) + { + _logger.LogWarning("Exception occurred during certificate replacement. Attempting to rollback virtual service bindings."); + + // Group backups by virtual server + port + protocol for rollback + var portBindings = virtualServiceBackups + .GroupBy(b => new { b.VirtualServerName, b.Port, b.Protocol }) + .ToList(); + + // For each unique port, build a single rollback update with both template bindings + foreach (var bindingGroup in portBindings) + { + try + { + var vsName = bindingGroup.Key.VirtualServerName; + var port = bindingGroup.Key.Port; + var protocol = bindingGroup.Key.Protocol; + + var update = new VirtualServerPortUpdate + { + PortNumber = port, + Protocol = protocol + }; + + foreach (var b in bindingGroup) + { + if (b.TemplateType.Equals("server-ssl", StringComparison.OrdinalIgnoreCase)) + update.TemplateServerSsl = b.OriginalTemplateName; + else if (b.TemplateType.Equals("client-ssl", StringComparison.OrdinalIgnoreCase)) + update.TemplateClientSsl = b.OriginalTemplateName; + else + throw new ArgumentException($"Unknown template type: {b.TemplateType}"); + } + + var bindRequest = new VirtualServerPortUpdateRequest { Port = update }; + var requestJson = JsonConvert.SerializeObject(bindRequest); + _logger.LogTrace($"Rolling back templates to VS={vsName} Port={port} Protocol={protocol} => {requestJson}"); + + apiClient.PutVirtualServerPort(vsName, port, protocol, requestJson); + } + catch (Exception rollbackEx) + { + _logger.LogError($"Failed to rollback binding for virtual service port {bindingGroup.Key.Port}: {LogHandler.FlattenException(rollbackEx)}"); + } + } + } + + _logger.LogError($"Error in Management.Replace: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + private string GenerateNewAlias(string originalAlias) + { + string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); + + // Match the first Unix timestamp in the format _########## (10 digits) + var match = System.Text.RegularExpressions.Regex.Match(originalAlias, @"^(.*?)(_?\d{10}).*$"); + + string newAlias; + + if (match.Success) + { + // Keep everything before the first timestamp, append new timestamp + newAlias = $"{match.Groups[1].Value}_{timestamp}"; + } + else + { + // No timestamp found, just append new one + newAlias = $"{originalAlias}_{timestamp}"; + } + + // Ensure it's within the 240 character limit + if (newAlias.Length > 240) + { + int maxBaseLength = 240 - (timestamp.Length + 1); // +1 for underscore + string basePart = newAlias.Substring(0, maxBaseLength); + newAlias = $"{basePart}_{timestamp}"; + } + + return newAlias; + } + + // Rest of the methods (Remove, Add) remain the same as they don't use the template update APIs + protected internal virtual void Remove(ManagementJobConfiguration configInfo, InventoryResult inventoryResult, + ApiClient apiClient) + { + try + { + _logger.LogTrace($"Start Delete the {configInfo.JobCertificate.Alias} Certificate and Private Key"); + + // First call: Delete the certificate + var deleteCertRoot = new DeleteCertBaseRequest + { + Delete = new DeleteCertRequest + { + CertName = configInfo.JobCertificate.Alias + } + }; + + apiClient.RemoveCertificate(deleteCertRoot); + _logger.LogTrace($"Successfully deleted certificate: {configInfo.JobCertificate.Alias}"); + + // Second call: Delete the private key (if it exists) + if (inventoryResult.InventoryList[0].PrivateKeyEntry) + { + var deleteKeyRoot = new DeleteCertBaseRequest + { + Delete = new DeleteCertRequest + { + PrivateKey = configInfo.JobCertificate.Alias + } + }; + + apiClient.RemovePrivateKey(deleteKeyRoot); + _logger.LogTrace($"Successfully deleted private key: {configInfo.JobCertificate.Alias}"); + } + + apiClient.WriteMemory(); + _logger.LogTrace($"Successful Delete of the {configInfo.JobCertificate.Alias} Certificate and 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}"); + } + apiClient.WriteMemory(); + _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; + } + } + } + + + // Helper class to store virtual service backup information + public class VirtualServiceBackup + { + public string VirtualServerName { get; set; } + public int? Port { get; set; } + public string Protocol { get; set; } + public string TemplateType { get; set; } // "server-ssl" or "client-ssl" + public string TemplateName { get; set; } + public string OriginalTemplateName { get; set; } + } +} \ No newline at end of file diff --git a/a10vthunder-orchestrator/InventoryResult.cs b/a10vthunder-orchestrator/InventoryResult.cs index 70fb23d..1c9d004 100644 --- a/a10vthunder-orchestrator/InventoryResult.cs +++ b/a10vthunder-orchestrator/InventoryResult.cs @@ -1,7 +1,21 @@ -using System.Collections.Generic; +// 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.Collections.Generic; using Keyfactor.Orchestrators.Extensions; -namespace a10vthunder_orchestrator +namespace a10vthunder { public class InventoryResult { diff --git a/a10vthunder-orchestrator/JobAttribute.cs b/a10vthunder-orchestrator/JobAttribute.cs index 035ac32..49710be 100644 --- a/a10vthunder-orchestrator/JobAttribute.cs +++ b/a10vthunder-orchestrator/JobAttribute.cs @@ -1,6 +1,20 @@ -using System; +// 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. -namespace a10vthunder_orchestrator +using System; + +namespace a10vthunder { [AttributeUsage(AttributeTargets.Class)] public class JobAttribute : Attribute diff --git a/a10vthunder-orchestrator/Jobs/Management.cs b/a10vthunder-orchestrator/Jobs/Management.cs deleted file mode 100644 index a3b6050..0000000 --- a/a10vthunder-orchestrator/Jobs/Management.cs +++ /dev/null @@ -1,297 +0,0 @@ -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/api/ApiClient.cs b/a10vthunder-orchestrator/api/ApiClient.cs index 891fd87..4584290 100644 --- a/a10vthunder-orchestrator/api/ApiClient.cs +++ b/a10vthunder-orchestrator/api/ApiClient.cs @@ -1,14 +1,28 @@ -using System; +// 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; using System.Collections.Generic; using System.IO; using System.Net; using System.Text; -using a10vthunder_orchestrator.Api.Models; +using a10vthunder.Api.Models; using Keyfactor.Logging; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace a10vthunder_orchestrator.Api +namespace a10vthunder.Api { public sealed class ApiClient : IDisposable { @@ -48,8 +62,8 @@ public void Dispose() public void Logon() { Logger.MethodEntry(); - var authRequest = new AuthRequest - {Credentials = new Credentials {Username = UserId, Password = Password}}; + var authRequest = new AuthRequest + { Credentials = new Credentials { Username = UserId, Password = Password } }; var strRequest = JsonConvert.SerializeObject(authRequest); try { @@ -106,6 +120,43 @@ public void AddCertificate(SslCertificateRequest sslCertRequest, string importCe } } + public void SetPartition(SetPartitionRequest sslSetPartitionRequest) + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"Set Partition Request: {JsonConvert.SerializeObject(sslSetPartitionRequest)}"); + ApiRequestString("POST", "/axapi/v3/active-partition", "POST", JsonConvert.SerializeObject(sslSetPartitionRequest), + false, true); + Logger.LogTrace("Set Partition Complete..."); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError( + $"Error In ApiClient.AddCertificate(SslCertificateRequest sslCertRequest, string importCertificate): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public void WriteMemory() + { + try + { + Logger.MethodEntry(); + ApiRequestString("POST", "/axapi/v3/write/memory", "POST", "", + false, true); + Logger.LogTrace("WriteMemory Complete..."); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError( + $"Error In ApiClient.WriteMemory: {LogHandler.FlattenException(ex)}"); + throw; + } + } + public void AddCertificate(SslCertificateRequest sslCertRequest, byte[] certData) { try @@ -221,6 +272,342 @@ public SslCollectionResponse GetCertificates(string certName = "") } } + public ServerTemplateListResponse GetServerTemplates() + { + try + { + Logger.MethodEntry(); + var strResponse = ApiRequestString("GET", $"/axapi/v3/slb/template/server-ssl-list", "GET", "", false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + var sslTemplateResponse = JsonConvert.DeserializeObject(strResponse); + Logger.LogTrace($"sslColResponse: {JsonConvert.SerializeObject(sslTemplateResponse)}"); + Logger.MethodExit(); + return sslTemplateResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In GetServerTemplates(): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public ClientTemplateListResponse GetClientTemplates() + { + try + { + Logger.MethodEntry(); + var strResponse = ApiRequestString("GET", $"/axapi/v3/slb/template/client-ssl-list", "GET", "", false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + var sslTemplateResponse = JsonConvert.DeserializeObject(strResponse); + Logger.LogTrace($"sslColResponse: {JsonConvert.SerializeObject(sslTemplateResponse)}"); + Logger.MethodExit(); + return sslTemplateResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In GetClientTemplates(): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + // Updated method to handle both V4 and V6 API versions + public UpdateServerTemplateResponse UpdateServerTemplates(UpdateTemplateRequest request, string templateName, ApiVersion version = ApiVersion.V6) + { + try + { + Logger.MethodEntry(); + + string endpoint; + string requestBody; + + if (version == ApiVersion.V6) + { + // V6 uses the certificate sub-resource endpoint + endpoint = $"/axapi/v3/slb/template/server-ssl/{templateName}/certificate"; + requestBody = JsonConvert.SerializeObject(request); + } + else // V4 + { + // V4 uses the main template endpoint and different request structure + endpoint = $"/axapi/v3/slb/template/server-ssl/{templateName}"; + + // Convert V6 request format to V4 format + var v4Request = new Dictionary + { + ["server-ssl"] = new Dictionary + { + ["cert"] = request.Certificate.Cert, + ["key"] = request.Certificate.Key + } + }; + requestBody = JsonConvert.SerializeObject(v4Request); + } + + var strResponse = ApiRequestString("PUT", endpoint, "PUT", requestBody, false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + + var sslTemplateResponse = ParseResponse(strResponse, version, "server-ssl"); + Logger.LogTrace($"sslTemplateResponse: {JsonConvert.SerializeObject(sslTemplateResponse)}"); + Logger.MethodExit(); + return sslTemplateResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In UpdateServerTemplates(UpdateTemplateRequest request, string templateName, ApiVersion version): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + // Helper method to parse responses based on API version + private UpdateServerTemplateResponse ParseResponse(string responseJson, ApiVersion version,string responseType) + { + if (version == ApiVersion.V6) + { + // V6 response structure + return JsonConvert.DeserializeObject(responseJson); + } + else // V4 + { + // V4 has a different response structure - extract what we need + var v4Response = JsonConvert.DeserializeObject(responseJson); + var clientSsl = v4Response[responseType]; + + return new UpdateServerTemplateResponse + { + certificate = new UpdateTemplateResposneCertificate + { + cert = clientSsl.cert, + key = clientSsl.key, + uuid = clientSsl.uuid, + a10url = clientSsl["a10-url"] + } + }; + } + } + + // Enum to specify API version + public enum ApiVersion + { + V4, + V6 + } + + // Alternative approach: Separate methods for each version + public UpdateServerTemplateResponse UpdateServerTemplatesV6(UpdateTemplateRequest request, string templateName) + { + try + { + Logger.MethodEntry(); + var strResponse = ApiRequestString("PUT", $"/axapi/v3/slb/template/server-ssl/{templateName}/certificate", "PUT", + JsonConvert.SerializeObject(request), false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + var sslTemplateResponse = JsonConvert.DeserializeObject(strResponse); + Logger.LogTrace($"sslTemplateResponse: {JsonConvert.SerializeObject(sslTemplateResponse)}"); + Logger.MethodExit(); + return sslTemplateResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In UpdateServerTemplatesV6: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public UpdateServerTemplateResponse UpdateServerTemplatesV4(UpdateTemplateRequest request, string templateName) + { + try + { + Logger.MethodEntry(); + + // Convert to V4 request format + var v4Request = new + { + client_ssl = new + { + name = templateName, + cert = request.Certificate.Cert, + key = request.Certificate.Key + } + }; + + var strResponse = ApiRequestString("PUT", $"/axapi/v3/slb/template/client-ssl/{templateName}", "PUT", + JsonConvert.SerializeObject(v4Request), false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + + // Parse V4 response and convert to our standard response format + var v4Response = JsonConvert.DeserializeObject(strResponse); + var clientSsl = v4Response["client-ssl"]; + + var sslTemplateResponse = new UpdateServerTemplateResponse + { + certificate = new UpdateTemplateResposneCertificate + { + cert = clientSsl.cert, + key = clientSsl.key, + uuid = clientSsl.uuid, + a10url = clientSsl["a10-url"] + } + }; + + Logger.LogTrace($"sslTemplateResponse: {JsonConvert.SerializeObject(sslTemplateResponse)}"); + Logger.MethodExit(); + return sslTemplateResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In UpdateServerTemplatesV4: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + // Updated method to handle both V4 and V6 API versions for client templates + public UpdateClientTemplateResponse UpdateClientTemplates(UpdateTemplateRequest request, string templateName, ApiVersion version = ApiVersion.V6) + { + try + { + Logger.MethodEntry(); + + string endpoint; + string requestBody; + + if (version == ApiVersion.V6) + { + // V6 uses the certificate sub-resource endpoint + endpoint = $"/axapi/v3/slb/template/client-ssl/{templateName}/certificate"; + requestBody = JsonConvert.SerializeObject(request); + } + else // V4 + { + // V4 uses the main template endpoint and different request structure + endpoint = $"/axapi/v3/slb/template/client-ssl/{templateName}"; + + // Convert V6 request format to V4 format + var v4Request = new Dictionary + { + ["client-ssl"] = new Dictionary + { + ["cert"] = request.Certificate.Cert, + ["key"] = request.Certificate.Key + } + }; + requestBody = JsonConvert.SerializeObject(v4Request); + } + + var strResponse = ApiRequestString("PUT", endpoint, "PUT", requestBody, false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + + var clientTemplateResponse = ParseClientResponse(strResponse, version); + Logger.LogTrace($"clientTemplateResponse: {JsonConvert.SerializeObject(clientTemplateResponse)}"); + Logger.MethodExit(); + return clientTemplateResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In UpdateClientTemplates(UpdateTemplateRequest request, string templateName, ApiVersion version): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + // Helper method to parse client template responses based on API version + private UpdateClientTemplateResponse ParseClientResponse(string responseJson, ApiVersion version) + { + if (version == ApiVersion.V6) + { + // V6 response structure - already matches our model + return JsonConvert.DeserializeObject(responseJson); + } + else // V4 + { + // V4 has a different response structure - convert to V6 format + var v4Response = JsonConvert.DeserializeObject(responseJson); + var clientSsl = v4Response["client-ssl"]; + + return new UpdateClientTemplateResponse + { + certificatelist = new List + { + new CleintCertificateList + { + cert = clientSsl.cert, + key = clientSsl.key, + uuid = clientSsl.uuid, + a10url = clientSsl["a10-url"] + } + } + }; + } + } + + // Alternative approach: Separate methods for each version + public UpdateClientTemplateResponse UpdateClientTemplatesV6(UpdateTemplateRequest request, string templateName) + { + try + { + Logger.MethodEntry(); + var strResponse = ApiRequestString("PUT", $"/axapi/v3/slb/template/client-ssl/{templateName}/certificate", "PUT", + JsonConvert.SerializeObject(request), false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + var clientTemplateResponse = JsonConvert.DeserializeObject(strResponse); + Logger.LogTrace($"clientTemplateResponse: {JsonConvert.SerializeObject(clientTemplateResponse)}"); + Logger.MethodExit(); + return clientTemplateResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In UpdateClientTemplatesV6: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public UpdateClientTemplateResponse UpdateClientTemplatesV4(UpdateTemplateRequest request, string templateName) + { + try + { + Logger.MethodEntry(); + + // Convert to V4 request format + var v4Request = new + { + client_ssl = new + { + cert = request.Certificate.Cert, + key = request.Certificate.Key + } + }; + + var strResponse = ApiRequestString("PUT", $"/axapi/v3/slb/template/client-ssl/{templateName}", "PUT", + JsonConvert.SerializeObject(v4Request), false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + + // Parse V4 response and convert to our standard response format + var v4Response = JsonConvert.DeserializeObject(strResponse); + var clientSsl = v4Response["client-ssl"]; + + var clientTemplateResponse = new UpdateClientTemplateResponse + { + certificatelist = new List + { + new CleintCertificateList + { + cert = clientSsl.cert, + key = clientSsl.key, + uuid = clientSsl.uuid, + a10url = clientSsl["a10-url"] + } + } + }; + + Logger.LogTrace($"clientTemplateResponse: {JsonConvert.SerializeObject(clientTemplateResponse)}"); + Logger.MethodExit(); + return clientTemplateResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In UpdateClientTemplatesV4: {LogHandler.FlattenException(ex)}"); + throw; + } + } + public string GetCertificate(string certificateName) { try @@ -240,22 +627,100 @@ public string GetCertificate(string certificateName) } } - public void RemoveCertificate(DeleteCertBaseRequest deleteCertRoot) + public void ReplaceCertificateAndKey(string certUrl, string keyUrl, string versionInfo) { + Logger.MethodEntry(); + try { - Logger.MethodEntry(); - Logger.LogTrace($"deleteCertRoot: {JsonConvert.SerializeObject(deleteCertRoot)}"); - ApiRequestString("POST", "/axapi/v3/pki/delete", "POST", JsonConvert.SerializeObject(deleteCertRoot), - false, true); - Logger.MethodExit(); + // 1. Upload certificate + var certRequest = new ManagementCertRequest + { + Certificate = new ManagementCertificate + { + Load = 1, + FileUrl = certUrl + } + }; + + Logger.LogInformation($"Uploading certificate from {certUrl}"); + ApiRequestString("POST", "/axapi/v3/web-service/secure/certificate", "POST", JsonConvert.SerializeObject(certRequest), false, true); + + // 2. Upload private key + var keyRequest = new ManagementPrivateKeyRequest + { + PrivateKey = new PrivateKey + { + Load = 1, + FileUrl = keyUrl + } + }; + + Logger.LogInformation($"Uploading private key from {keyUrl}"); + ApiRequestString("POST", "/axapi/v3/web-service/secure/private-key", "POST", JsonConvert.SerializeObject(keyRequest), false, true); + + var restartRequest = new ManagementCertRestartRequest + { + Secure = new Secure + { + Restart = 1 + } + }; + + Logger.LogInformation("Restarting secure system to apply certificate and key."); + ApiRequestString("POST", "/axapi/v3/web-service/secure", "POST", JsonConvert.SerializeObject(restartRequest), false, true); + + Logger.LogInformation("Certificate and key replaced and secure."); + } + catch (WebException webEx) when (webEx.InnerException?.InnerException?.Message.Contains("ResponseEnded")==true)//Version 4 has a socket hang up error issue but it still works + { + Logger.LogInformation("Web service restart completed (connection closed as expected)"); + // This is expected behavior when restarting the web service } catch (Exception ex) { - Logger.LogError( - $"Error In RemoveCertificate(DeleteCertBaseRequest deleteCertRoot): {LogHandler.FlattenException(ex)}"); + Logger.LogError($"Error replacing certificate and key: {LogHandler.FlattenException(ex)}"); throw; } + + Logger.MethodExit(); + } + + + 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 void RemovePrivateKey(DeleteCertBaseRequest deleteKeyRoot) + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"deleteKeyRoot: {JsonConvert.SerializeObject(deleteKeyRoot)}"); + ApiRequestString("POST", "/axapi/v3/pki/delete", "POST", JsonConvert.SerializeObject(deleteKeyRoot), + false, true); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError( + $"Error In RemovePrivateKey(DeleteCertBaseRequest deleteKeyRoot): {LogHandler.FlattenException(ex)}"); + throw; + } } public HttpWebRequest CreateRequest(string baseUrl, string postUrl) @@ -264,7 +729,7 @@ public HttpWebRequest CreateRequest(string baseUrl, string postUrl) { Logger.MethodEntry(); Logger.LogTrace($"baseUrl: {baseUrl} postUrl: {postUrl}"); - var objRequest = (HttpWebRequest) WebRequest.Create(BaseUrl + postUrl); + var objRequest = (HttpWebRequest)WebRequest.Create(BaseUrl + postUrl); Logger.MethodExit(); return objRequest; } @@ -282,7 +747,7 @@ public HttpWebResponse GetResponse(HttpWebRequest request) { Logger.MethodEntry(); Logger.MethodExit(); - return (HttpWebResponse) request.GetResponse(); + return (HttpWebResponse)request.GetResponse(); } catch (Exception ex) { @@ -291,54 +756,60 @@ public HttpWebResponse GetResponse(HttpWebRequest request) } } - public string ApiRequestString(string strCall, string strPostUrl, string strMethod, - string strQueryString, - bool bWrite, bool bUseToken) + 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") + if (!string.IsNullOrEmpty(strQueryString) && (strMethod == "POST" || strMethod == "PUT")) { 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); + + using var response = (HttpWebResponse)objRequest.GetResponse(); + + using var strReader = new StreamReader(response.GetResponseStream() ?? Stream.Null); var strResponse = strReader.ReadToEnd(); + + // ✅ Check for non-2xx codes (shouldn’t happen here, but just in case) + if ((int)response.StatusCode < 200 || (int)response.StatusCode >= 300) + { + HandleApiError(strResponse, (int)response.StatusCode); + } + Logger.MethodExit(); return strResponse; } + catch (WebException webEx) when (webEx.Response is HttpWebResponse errorResponse) + { + using var errorReader = new StreamReader(errorResponse.GetResponseStream() ?? Stream.Null); + var errorBody = errorReader.ReadToEnd(); + HandleApiError(errorBody, (int)errorResponse.StatusCode); + throw; + } catch (Exception ex) { - Logger.LogError($"Error In ApiRequestString: {LogHandler.FlattenException(ex)}"); + Logger.LogError($"Unhandled Error In ApiRequestString: {LogHandler.FlattenException(ex)}"); throw; } } + public HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary postParameters) { @@ -472,6 +943,130 @@ public FileParameter(byte[] file, string filename, string contentType) public string ContentType { get; set; } } + private void HandleApiError(string errorBody, int httpStatusCode) + { + try + { + var errorObj = JsonConvert.DeserializeObject(errorBody); + string message = errorObj?.response?.err?.msg ?? "Unknown error"; + int code = errorObj?.response?.err?.code ?? 0; + + var fullMessage = $"A10 API Error: HTTP {httpStatusCode} - {message} (Code: {code})"; + Logger.LogError(fullMessage); + + throw new ApplicationException(fullMessage); + } + catch (Exception ex) + { + Logger.LogError($"Failed to parse API error body: {errorBody}"); + throw new ApplicationException($"HTTP {httpStatusCode} - Unexpected API error format", ex); + } + } + + // Add these methods to your ApiClient class + + public VirtualServerListResponse GetVirtualServices() + { + try + { + Logger.MethodEntry(); + var strResponse = ApiRequestString("GET", "/axapi/v3/slb/virtual-server", "GET", "", false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + var virtualServerResponse = JsonConvert.DeserializeObject(strResponse); + Logger.LogTrace($"virtualServerResponse: {JsonConvert.SerializeObject(virtualServerResponse)}"); + Logger.MethodExit(); + return virtualServerResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In GetVirtualServices(): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public void UnbindTemplateFromVirtualService(string virtualServerName, int? port, string protocol, string templateType) + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"Unbinding {templateType} template from virtual server: {virtualServerName}, port: {port}, protocol: {protocol}"); + + var unbindRequest = new VirtualServerPortUpdateRequest(); + + if (templateType.Equals("server-ssl", StringComparison.OrdinalIgnoreCase)) + { + unbindRequest.Port = new VirtualServerPortUpdate + { + PortNumber = port, + Protocol = protocol, + TemplateServerSsl = null // Empty string to unbind + }; + } + else if (templateType.Equals("client-ssl", StringComparison.OrdinalIgnoreCase)) + { + unbindRequest.Port = new VirtualServerPortUpdate + { + PortNumber = port, + Protocol = protocol, + TemplateClientSsl = null // Empty string to unbind + }; + } + else + { + throw new ArgumentException($"Unsupported template type: {templateType}"); + } + + var requestJson = JsonConvert.SerializeObject(unbindRequest); + Logger.LogTrace($"Unbind request: {requestJson}"); + + ApiRequestString("PUT", $"/axapi/v3/slb/virtual-server/{virtualServerName}/port/{port}+{protocol}", "PUT", requestJson, false, true); + + Logger.LogTrace($"Successfully unbound {templateType} template from virtual server {virtualServerName}"); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError($"Error In UnbindTemplateFromVirtualService: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public VersionResponse GetVersion() + { + try + { + Logger.MethodEntry(); + var strResponse = ApiRequestString("GET", "/axapi/v3/version/oper", "GET", "", false, true); + Logger.LogTrace($"strResponse: {strResponse}"); + var versionResponse = JsonConvert.DeserializeObject(strResponse); + Logger.LogTrace($"versionResponse: {JsonConvert.SerializeObject(versionResponse)}"); + Logger.MethodExit(); + return versionResponse; + } + catch (Exception ex) + { + Logger.LogError($"Error In GetVersion(): {LogHandler.FlattenException(ex)}"); + throw; + } + } + + public void PutVirtualServerPort(string virtualServerName, int? port, string protocol, string jsonBody) + { + try + { + Logger.MethodEntry(); + Logger.LogTrace($"PUT /axapi/v3/slb/virtual-server/{virtualServerName}/port/{port}+{protocol} BODY: {jsonBody}"); + ApiRequestString("PUT", $"/axapi/v3/slb/virtual-server/{virtualServerName}/port/{port}+{protocol}", "PUT", jsonBody, false, true); + Logger.MethodExit(); + } + catch (Exception ex) + { + Logger.LogError($"Error in PutVirtualServerPort: {LogHandler.FlattenException(ex)}"); + throw; + } + } + + #endregion } } \ No newline at end of file diff --git a/a10vthunder-orchestrator/manifest.json b/a10vthunder-orchestrator/manifest.json index ef7f4ad..5e3a76a 100644 --- a/a10vthunder-orchestrator/manifest.json +++ b/a10vthunder-orchestrator/manifest.json @@ -1,13 +1,21 @@ { "extensions": { "Keyfactor.Orchestrators.Extensions.IOrchestratorJobExtension": { - "CertStores.vThunderU.Inventory": { - "assemblypath": "a10vthunder-orchestrator.dll", - "TypeFullName": "a10vthunder_orchestrator.Jobs.Inventory" + "CertStores.ThunderMgmt.Inventory": { + "assemblypath": "A10vThunder.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderMgmt.Inventory" }, - "CertStores.vThunderU.Management": { - "assemblypath": "a10vthunder-orchestrator.dll", - "TypeFullName": "a10vthunder_orchestrator.Jobs.Management" + "CertStores.ThunderMgmt.Management": { + "assemblypath": "A10vThunder.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderMgmt.Management" + }, + "CertStores.ThunderSsl.Inventory": { + "assemblypath": "A10vThunder.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderSsl.Inventory" + }, + "CertStores.ThunderSsl.Management": { + "assemblypath": "A10vThunder.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.A10vThunder.ThunderSsl.Management" } } } diff --git a/docsource/content.md b/docsource/content.md new file mode 100644 index 0000000..1434f19 --- /dev/null +++ b/docsource/content.md @@ -0,0 +1,208 @@ +## Overview + +A Keyfactor Universal Orchestrator extension for managing SSL/TLS certificates on A10 Networks vThunder load balancers through direct API integration and SCP-based management interface certificate deployment. + +The A10 vThunder Orchestrator provides automated certificate lifecycle management for A10 Networks vThunder appliances. It implements two distinct certificate store types: + +1. **ThunderSsl**: Direct API-based management of SSL certificates for load balancing and application delivery +2. **ThunderMgmt**: SCP-based management of certificates for the A10 management interface (GUI/API access) + +### Architecture + +The A10 vThunder Orchestrator implements two distinct certificate store types with different architectural approaches: +```mermaid +graph TB + subgraph "Keyfactor Environment" + KF[Keyfactor Command] + UO[Universal Orchestrator
with A10 Extension] + end + + subgraph "ThunderSsl Store Type - Direct API Management" + A10_SSL[A10 vThunder Appliance
API Endpoint] + SSL_STORE[(SSL Certificate Store
Certificates & Keys)] + SSL_TEMPLATES[SSL Templates
server-ssl / client-ssl] + VIRTUAL_SERVICES[Virtual Services
Load Balancer Config] + end + + subgraph "ThunderMgmt Store Type - SCP-Based Management" + SCP[SCP Server
Intermediate Storage] + A10_MGMT[A10 vThunder Appliance
Management Interface] + MGMT_STORE[(Management Certs
.crt / .key files)] + end + + KF -->|Certificate Lifecycle Jobs| UO + + UO -->|"1. AXAPI REST Calls
(HTTPS - v4/v6)
Auth, Upload, Template Updates"| A10_SSL + A10_SSL -->|Manages| SSL_STORE + A10_SSL -->|Updates Bindings| SSL_TEMPLATES + SSL_TEMPLATES -->|Bound To| VIRTUAL_SERVICES + + UO -->|"2a. SCP Upload
(SSH/SCP)
cert.crt + cert.key"| SCP + SCP -->|"2b. A10 Retrieves
(SCP/SSH)"| A10_MGMT + UO -->|"2c. AXAPI Install Command
(HTTPS)"| A10_MGMT + A10_MGMT -->|Installs| MGMT_STORE + + style UO fill:#4CAF50,stroke:#2E7D32,color:#fff + style A10_SSL fill:#2196F3,stroke:#1565C0,color:#fff + style A10_MGMT fill:#2196F3,stroke:#1565C0,color:#fff + style SCP fill:#FF9800,stroke:#E65100,color:#fff + style SSL_STORE fill:#9C27B0,stroke:#6A1B9A,color:#fff + style MGMT_STORE fill:#9C27B0,stroke:#6A1B9A,color:#fff +``` + +#### ThunderSsl Store Type (Direct API Management) + +The ThunderSsl store type provides direct, API-based certificate management: + +- **Single-hop architecture**: Orchestrator connects directly to A10 AXAPI (REST API) via HTTPS +- **Automatic template management**: Detects and updates SSL template bindings (server-ssl/client-ssl) +- **Zero-downtime replacements**: Creates timestamped certificates and atomically rebinds templates +- **Multi-tenant support**: Full partition support for isolated certificate operations +- **API version flexibility**: Automatically detects and supports both AXAPI v4 and v6 + +#### ThunderMgmt Store Type (SCP-Based Management) + +The ThunderMgmt store type uses an intermediate SCP server for management interface certificates: + +- **Three-party architecture**: Orchestrator → SCP Server → A10 Device +- **Network flexibility**: Supports different network paths between orchestrator and A10 device +- **File-based deployment**: Uploads .crt and .key files to SCP server for A10 retrieval +- **Management interface specific**: Used exclusively for A10 GUI/API access certificates +- **Coordinated installation**: AXAPI commands trigger certificate installation after file transfer + +Both store types support PAM integration for secure credential management and require appropriate A10 device permissions. + +#### ThunderSsl Store Type +Uses A10's native REST API (AXAPI) for direct certificate management: +- Certificates are uploaded directly to the A10 appliance +- Supports both certificate-only and certificate-with-private-key operations +- Automatically detects and handles template bindings and virtual service configurations +- Implements intelligent certificate replacement to avoid service disruption + +#### A10 vThunder Requirements +- A10 vThunder appliance with AXAPI support +- API versions 4.x or 6.x supported (automatically detected) +- Valid user account with certificate management privileges +- For ThunderMgmt: SSH/SCP access enabled + +#### Required A10 Permissions +The orchestrator requires an A10 user account with permissions to: +- Access AXAPI (REST API) +- Manage SSL certificates and private keys +- Read/write SSL templates (server-ssl and client-ssl) +- Query and modify virtual services +- Write configuration to memory +- Set active partitions +- For ThunderMgmt: SSH/SCP file operations + +### Key Features + +- **Direct SSL Certificate Management**: Native A10 API integration for SSL certificate deployment and management +- **Template-Aware Operations**: Intelligent handling of certificates bound to SSL templates and virtual services +- **Multi-API Version Support**: Automatic detection and support for A10 API v4 and v6 +- **Partition Support**: Full support for A10 partitions for multi-tenant deployments +- **Certificate Inventory**: Comprehensive discovery and inventory of existing certificates +- **Management Interface Certificates**: SCP-based deployment for A10 management interface certificates +- **PAM Integration**: Support for Privileged Access Management systems +- **Advanced Certificate Replacement**: Zero-downtime certificate replacement with automatic template rebinding + +## API Integration Details + +### AXAPI Endpoints Used + +- **Authentication**: `/axapi/v3/auth` or `/axapi/v4/auth` +- **SSL Certificates**: `/axapi/v3/slb/ssl-cert` or `/axapi/v4/slb/ssl-cert` +- **Private Keys**: `/axapi/v3/slb/ssl-key` or `/axapi/v4/slb/ssl-key` +- **SSL Templates**: `/axapi/v3/slb/template/server-ssl` and `/axapi/v3/slb/template/client-ssl` +- **Virtual Services**: `/axapi/v3/slb/virtual-server` +- **Partitions**: `/axapi/v3/active-partition` +- **Memory Operations**: `/axapi/v3/write/memory` + +### Advanced Features + +#### Partition Support + +The orchestrator fully supports A10 partitions: +- Set active partition before operations +- Isolate certificate operations to specific partitions +- Support multi-tenant deployments + +#### Template Management + +Intelligent SSL template handling: +- Detection of server-ssl and client-ssl template usage +- Atomic template updates during certificate replacement +- Preservation of template configurations + +#### Virtual Service Coordination + +Advanced virtual service management: +- Mapping of templates to virtual service ports +- Coordinated unbinding and rebinding operations +- Support for multiple template types on single ports + +### TEST CASES + +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +1|Fresh Add SSL Certificate With Private Key|Will create new SSL certificate and private key on the vThunder appliance in shared partition|shared|true|WebServerSSL|The new WebServerSSL certificate and private key will be created in SSL certificate store on vThunder|True +1a|Replace SSL Cert with no overwrite flag|Should warn user that a cert cannot be replaced with the same name without overwrite flag|shared|false|WebServerSSL|Error message indicating overwrite flag must be used|True +1b|Replace SSL Cert with overwrite flag (unbound)|Will replace certificate and private key on vThunder for unbound certificate|shared|true|WebServerSSL|Certificate will be removed and re-added because it's not bound to templates|True +2|Add SSL Cert Without Private Key|This will create a certificate with no private key on vThunder|shared|false|PublicCertOnly|Only certificate will be added to vThunder SSL store with no private key|True +2a|Replace SSL Cert Without Private Key|This will replace a certificate with no private key on vThunder|shared|true|PublicCertOnly|Only certificate will be replaced on vThunder with no private key|True +2b|Replace SSL Cert Without Private Key no overwrite flag|Should warn user that a cert cannot be replaced with the same name without overwrite flag|shared|false|PublicCertOnly|Error message indicating overwrite flag must be used|True +3|Remove Unbound SSL Certificate and Private Key|Certificate and Private Key will be removed from A10|shared|N/A|WebServerSSL|Certificate and key will be removed from vThunder SSL store|True +3a|Remove SSL Certificate without Private Key|Certificate will be removed from A10|shared|N/A|PublicCertOnly|Certificate will be removed from vThunder SSL store|True + +### Template-Bound Certificate Operations + +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +4|Replace Server-SSL Template-Bound Certificate|Will create new timestamped certificate and update server-ssl templates|shared|true|APIGatewayCert|New certificate created with timestamp alias (APIGatewayCert_1672531200), server-ssl templates updated, virtual services rebound, old cert removed|True +4a|Replace Client-SSL Template-Bound Certificate|Will create new timestamped certificate and update client-ssl templates|shared|true|ClientAuthCert|New certificate created with timestamp alias (ClientAuthCert_1672531200), client-ssl templates updated, virtual services rebound, old cert removed|True +4b|Replace Multi-Template-Bound Certificate|Will create new timestamped certificate and update both server-ssl and client-ssl templates|shared|true|DualPurposeCert|New certificate created with timestamp, both template types updated with consistent alias, all virtual services rebound|True +4c|Attempt to Remove Template-Bound Certificate|Should fail with informative error about certificate being in use|shared|N/A|BoundServerCert|Error indicating certificate is bound to SSL templates and cannot be removed|True + +### Partition Operations + +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +5|Add SSL Certificate to Custom Partition|Certificate will be added to specified partition instead of shared|tenant-prod|false|TenantWebCert|Certificate added to "tenant-prod" partition, isolated from shared partition|True +5a|Remove SSL Certificate from Custom Partition|Certificate will be removed from specified partition|tenant-prod|N/A|TenantWebCert|Certificate removed from "tenant-prod" partition, shared partition unaffected|True +5b|Replace Certificate in Custom Partition with Template Binding|Certificate replacement with template updates in specific partition|tenant-prod|true|TenantAPICert|New timestamped certificate created in partition, partition-specific templates updated|True + +### Inventory Operations + +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +6|Inventory SSL Certificates from Shared Partition|Inventory of SSL certificates will be pulled from shared partition|shared|N/A|N/A|All SSL certificates in shared partition inventoried with private key flags and metadata|True +6a|Inventory SSL Certificates from Custom Partition|Inventory of SSL certificates will be pulled from specified partition|tenant-prod|N/A|N/A|All SSL certificates in "tenant-prod" partition inventoried, isolated from other partitions|True +6b|Inventory Mixed Certificate Types|Inventory should handle certificates with and without private keys|shared|N/A|N/A|Certificates with private keys marked as PrivateKeyEntry=true, certificates without marked as false|True + +### API Version Compatibility + +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +7|API v4 Detection and Template Operations|System should detect A10 software version 4.x and use appropriate API format for template updates|shared|true|V4TestCert|API v4 format detected and used for template updates, version info logged showing 4.x software|True +7a|API v6 Detection and Template Operations|System should detect A10 software version 6.x and use appropriate API format for template updates|shared|true|V6TestCert|API v6 format detected and used for template updates (default), version info logged|True + +## ThunderMgmt Store Type Test Cases + +### SCP Certificate Operations + +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +8|Fresh Add Management Certificate via SCP|Will upload certificate files to SCP server and install on A10 management interface|/home/certuser|true|MgmtInterface2025|Files MgmtInterface2025.crt and MgmtInterface2025.key created on SCP server, A10 loads certificate via API|True +8a|Replace Management Certificate with overwrite flag|Will replace existing certificate files and reload on A10 management interface|/home/certuser|true|MgmtInterface2025|Existing files overwritten, A10 reloads certificate, 60-second delay observed, memory written|True +8b|Replace Management Certificate without overwrite flag|Should warn user that files cannot be replaced without overwrite flag|/home/certuser|false|MgmtInterface2025|Error indicating files exist and overwrite flag must be used|True +9|Add Management Cert Without Private Key|This will create certificate file only on SCP server|/home/certuser|false|MgmtCertOnly|Only .crt file will be created on SCP server, no .key file, A10 API called for certificate only|True +10|Remove Management Certificate Files|Certificate files will be removed from SCP server|/home/certuser|N/A|MgmtInterface2025|Both .crt and .key files deleted from SCP server, A10 management configuration unchanged|True + +### SCP Server Connectivity and Error Handling + +Case Number|Case Name|Case Description|Store Path|Overwrite Flag|Alias Name|Expected Results|Passed +-----------|---------|----------------|----------|--------------|----------|----------------|-------------- +11|Inventory Management Certificates from SCP|Inventory of certificate files will be retrieved from SCP server directory|/home/certuser|N/A|N/A|All valid PEM certificates in SCP directory inventoried, invalid files skipped gracefully|True +11a|SCP Authentication Failure|Should handle SCP authentication errors gracefully|/home/certuser|N/A|TestCert|Clear authentication error message, operation fails safely, security not compromised|True +11b|SCP Network Connectivity Issues|Should handle network connectivity issues to SCP server|/home/unreachable|N/A|TestCert|Network timeout error captured, distinguishes from authentication errors, provides troubleshooting guidance|True +11c|Remote File Already Exists Check|Should properly detect existing files on SCP server before upload|/home/certuser|false|ExistingCert|File existence check works correctly, appropriate error when overwrite=false and file exists|True diff --git a/docsource/images/ThunderMgmt-advanced-store-type-dialog.png b/docsource/images/ThunderMgmt-advanced-store-type-dialog.png new file mode 100644 index 0000000..7ce7813 Binary files /dev/null and b/docsource/images/ThunderMgmt-advanced-store-type-dialog.png differ diff --git a/docsource/images/ThunderMgmt-basic-store-type-dialog.png b/docsource/images/ThunderMgmt-basic-store-type-dialog.png new file mode 100644 index 0000000..fb61d69 Binary files /dev/null and b/docsource/images/ThunderMgmt-basic-store-type-dialog.png differ diff --git a/docsource/images/ThunderMgmt-custom-field-A10ToScpServerIp-dialog.png b/docsource/images/ThunderMgmt-custom-field-A10ToScpServerIp-dialog.png new file mode 100644 index 0000000..71903fd Binary files /dev/null and b/docsource/images/ThunderMgmt-custom-field-A10ToScpServerIp-dialog.png differ diff --git a/docsource/images/ThunderMgmt-custom-field-OrchToScpServerIp-dialog.png b/docsource/images/ThunderMgmt-custom-field-OrchToScpServerIp-dialog.png new file mode 100644 index 0000000..46540bd Binary files /dev/null and b/docsource/images/ThunderMgmt-custom-field-OrchToScpServerIp-dialog.png differ diff --git a/docsource/images/ThunderMgmt-custom-field-ScpPassword-dialog.png b/docsource/images/ThunderMgmt-custom-field-ScpPassword-dialog.png new file mode 100644 index 0000000..8fffc7f Binary files /dev/null and b/docsource/images/ThunderMgmt-custom-field-ScpPassword-dialog.png differ diff --git a/docsource/images/ThunderMgmt-custom-field-ScpPort-dialog.png b/docsource/images/ThunderMgmt-custom-field-ScpPort-dialog.png new file mode 100644 index 0000000..0f42859 Binary files /dev/null and b/docsource/images/ThunderMgmt-custom-field-ScpPort-dialog.png differ diff --git a/docsource/images/ThunderMgmt-custom-field-ScpUserName-dialog.png b/docsource/images/ThunderMgmt-custom-field-ScpUserName-dialog.png new file mode 100644 index 0000000..578f7c9 Binary files /dev/null and b/docsource/images/ThunderMgmt-custom-field-ScpUserName-dialog.png differ diff --git a/docsource/images/ThunderMgmt-custom-field-allowInvalidCert-dialog.png b/docsource/images/ThunderMgmt-custom-field-allowInvalidCert-dialog.png new file mode 100644 index 0000000..74bd63a Binary files /dev/null and b/docsource/images/ThunderMgmt-custom-field-allowInvalidCert-dialog.png differ diff --git a/docsource/images/ThunderMgmt-custom-fields-store-type-dialog.png b/docsource/images/ThunderMgmt-custom-fields-store-type-dialog.png new file mode 100644 index 0000000..9b9976a Binary files /dev/null and b/docsource/images/ThunderMgmt-custom-fields-store-type-dialog.png differ diff --git a/docsource/images/ThunderSsl-advanced-store-type-dialog.png b/docsource/images/ThunderSsl-advanced-store-type-dialog.png new file mode 100644 index 0000000..4827627 Binary files /dev/null and b/docsource/images/ThunderSsl-advanced-store-type-dialog.png differ diff --git a/docsource/images/ThunderSsl-basic-store-type-dialog.png b/docsource/images/ThunderSsl-basic-store-type-dialog.png new file mode 100644 index 0000000..73f1e80 Binary files /dev/null and b/docsource/images/ThunderSsl-basic-store-type-dialog.png differ diff --git a/docsource/images/ThunderSsl-custom-field-allowInvalidCert-dialog.png b/docsource/images/ThunderSsl-custom-field-allowInvalidCert-dialog.png new file mode 100644 index 0000000..048930a Binary files /dev/null and b/docsource/images/ThunderSsl-custom-field-allowInvalidCert-dialog.png differ diff --git a/docsource/images/ThunderSsl-custom-fields-store-type-dialog.png b/docsource/images/ThunderSsl-custom-fields-store-type-dialog.png new file mode 100644 index 0000000..a1faa77 Binary files /dev/null and b/docsource/images/ThunderSsl-custom-fields-store-type-dialog.png differ diff --git a/docsource/thundermgmt.md b/docsource/thundermgmt.md new file mode 100644 index 0000000..2f5b117 --- /dev/null +++ b/docsource/thundermgmt.md @@ -0,0 +1,149 @@ +## Overview + +### 🔐 Management Certificates + +**Purpose:** +Used to secure HTTPS access to the A10 management interface (GUI/API). + +**Usage Context:** +- AXAPI (API access over HTTPS) +- Web GUI login +- Any administrative HTTPS session + +**Configured In:** +- **GUI:** `System → Settings → Certificate` + +**Example:** +When a user logs into the GUI via `https://`, the certificate presented is the **Management Certificate**. + +## Requirements + + +### A10 Certificate Management Orchestrator Extension + +This orchestrator extension automates the process of uploading, inventorying, and deploying SSL certificates from a Linux SCP server to an A10 vThunder device. Due to A10 API limitations, certificates must be pulled from the SCP server directly by the A10 device itself. + +--- + +#### 📌 How It Works + +1. **The orchestrator** connects to a Linux server via SCP to inventory available certificates. +2. It stores relevant metadata and pushes new certificates and keys to the SCP server. +3. It then instructs the **A10 device** to retrieve the certificate and private key from the Linux server using API calls. +4. The A10 device loads the certificate and key directly from the SCP server for use on its **management interface**. + +--- + +#### 📡 API Call Example (From A10 Device) + +```http +POST /axapi/v3/web-service/secure/certificate +``` + +**Payload:** +```json +{ + "certificate": { + "load": 1, + "file-url": "scp://ec2-user:dda@172.31.93.107:/home/ec2-user/26125.crt" + } +} +``` + +> A similar call is made for loading the private key onto the A10 device using a separate AXAPI endpoint. + +- The A10 device **must have access** to the SCP server via the specified IP (`A10ToScpServerIp`). +- Ensure the certificate and key file paths are correct and accessible to the SCP user. + +--- + +#### 🔐 Linux Server Requirements + +##### User Access +- The SCP user (`ScpUserName`, e.g., `ec2-user`) must: + - Have SSH/SCP access. + - Authenticate with a password. + - Have **read and write** permissions in the SCP location. + +> New certificates and **private keys** are generated by Keyfactor and uploaded to this location by the orchestrator. Therefore, write access is essential. + +##### SCP Directory Permissions +- Ensure the directory (e.g., `/home/ec2-user/`) is: + - Writable by the orchestrator (to upload new certs/keys). + - Readable by both the orchestrator and the A10 device (via SCP). + +--- + +#### 🔄 Alternate Design Consideration + +It may be possible to use the A10 device itself as the SCP target location if it supports read/write SCP operations **outside the CLI context**. However, A10 devices typically restrict file access through CLI or API mechanisms only, and not through standard SCP server operations. This limitation is why a separate Linux SCP server is currently required. + +--- + +#### 🔓 Network and Port Requirements + +| Source | Destination | Port | Protocol | Purpose | +|--------------------|---------------------|------|----------|-------------------------------| +| Orchestrator | Linux SCP Server | 22 | TCP | Inventory and upload via SCP | +| A10 Device | Linux SCP Server | 22 | TCP | Cert and key retrieval via SCP| +| Orchestrator/Admin | A10 Device (API) | 443 | HTTPS | API calls to load certificate | + +--- + +#### ThunderMgmt Aliases + +In the ThunderMgmt store type, the **alias** determines the filename for certificates stored on the SCP server: + +- **Certificate File**: `{alias}.crt` on the SCP server +- **Private Key File**: `{alias}.key` on the SCP server +- **A10 API Reference**: The A10 management interface loads certificates using SCP URLs pointing to these files + +##### Example ThunderMgmt Usage +``` +Alias: "mgmt-interface-cert" +→ SCP Server Files: + - /home/scpuser/mgmt-interface-cert.crt + - /home/scpuser/mgmt-interface-cert.key +→ A10 API Call: + - Certificate URL: scp://scpuser:pass@192.168.1.100:/home/scpuser/mgmt-interface-cert.crt + - Key URL: scp://scpuser:pass@192.168.1.100:/home/scpuser/mgmt-interface-cert.key +``` +##### For Alias Names +- Use names that clearly identify the management purpose: `mgmt-interface-2025` +- Ensure filenames are valid for both SCP server filesystem and A10 API calls +- Consider including renewal dates: `mgmt-cert-jan2025` + +##### ThunderMgmt File Management + +The orchestrator handles file operations as follows: + +1. **Add Operation**: + - Uploads `{alias}.crt` and `{alias}.key` to SCP server + - Calls A10 API to load certificate from SCP URLs + - A10 device pulls files directly from SCP server + +2. **Remove Operation**: + - Deletes `{alias}.crt` and `{alias}.key` from SCP server + - Does not modify A10 management interface configuration + +3. **Replace Operation** (with Overwrite=true): + - Overwrites existing `{alias}.crt` and `{alias}.key` files + - Calls A10 API to reload certificate from same SCP URLs + +##### Character Limitations +- **Maximum Length**: 240 characters (enforced by orchestrator) +- **Recommended Characters**: Letters, numbers, hyphens, underscores +- **Avoid**: Special characters that might cause issues in API calls or file operations + +##### ThunderMgmt Common Issues +- **File Path Issues**: Ensure SCP user has access to the target directory +- **Invalid Filenames**: Some characters may not be valid for filesystem operations +- **URL Encoding**: Special characters in aliases may require URL encoding in SCP URLs + +#### ✅ Summary + +This extension coordinates certificate and private key delivery by using SCP as a bridge between orchestrator logic and A10's strict API requirements. It ensures secure and automated deployment for the management interface certificates with minimal manual intervention. + + + + diff --git a/docsource/thunderssl.md b/docsource/thunderssl.md new file mode 100644 index 0000000..9701ba2 --- /dev/null +++ b/docsource/thunderssl.md @@ -0,0 +1,149 @@ +## Overview + +### 🔒 SSL Certificates + +**Purpose:** +Used for securing traffic that passes through the device (i.e., traffic handled by SLB/ADC features). + +**Usage Context:** +- SSL Offloading +- SSL Intercept (Decryption/Encryption) +- Reverse proxy configurations + +**Configured In:** +- **GUI:** `ADC → Ssl Management + + +**Example:** +If the A10 is acting as an SSL offloader for a backend web server, the **SSL Certificate** is used to terminate client HTTPS sessions. + + +## Requirements + +### Creating a User for API Access on A10 vThunder + +This guide explains how to create a user on A10 vThunder for API (AXAPI) access with appropriate privileges. + +#### Step-by-Step Instructions + +1. **Enter configuration mode:** + ```bash + configure terminal + ``` + +2. **Create the user and set a password:** + ```bash + admin apiuser password yourStrongPassword + ``` + + Replace `apiuser` with the desired username, and `yourStrongPassword` with a secure password. + +3. **Assign necessary privileges:** + ```bash + privilege read + privilege write + privilege partition-enable-disable + privilege partition-read + privilege partition-write + ``` + + These privileges grant the user: + - Global read and write access + - Per-partition read and write access + - Permission to enable or disable partitions + +4. **(Optional) Enable external health monitor privilege (if needed):** + ```bash + privilege hm + ``` + +5. **Exit user configuration:** + ```bash + exit + ``` + +#### ThunderSsl Aliases + +In the ThunderSsl store type, the **alias** directly corresponds to the certificate and private key names stored on the A10 appliance: + +- **Certificate Name**: The alias becomes the SSL certificate identifier in A10's certificate store +- **Private Key Name**: The same alias is used for the associated private key +- **Template References**: SSL templates reference certificates by this exact alias name +- **API Operations**: All A10 API calls use this alias to identify the certificate/key pair + +##### Example ThunderSsl Usage +``` +Alias: "webserver-prod-2025" +→ A10 Certificate: "webserver-prod-2025" +→ A10 Private Key: "webserver-prod-2025" +→ Template Reference: server-ssl template uses cert "webserver-prod-2025" +``` + +##### Alias Renaming for Template-Bound Certificates + +When replacing a certificate that's bound to SSL templates, the orchestrator uses an intelligent renaming strategy: + +1. **Timestamp Generation**: Creates a Unix timestamp (10 digits) +2. **Alias Pattern Matching**: + - If alias contains existing timestamp: `webserver-prod_1640995200` → `webserver-prod_1672531200` + - If no timestamp found: `webserver-prod` → `webserver-prod_1672531200` +3. **Length Validation**: Ensures final alias stays within A10's 240-character limit +4. **Template Updates**: All SSL templates are updated to reference the new timestamped alias +5. **Cleanup**: Original certificate is removed after successful template updates + +##### Replacement Workflow Example +``` +Original: "api-gateway-cert" +Step 1: Generate new alias → "api-gateway-cert_1672531200" +Step 2: Upload certificate with new alias +Step 3: Update server-ssl templates: cert "api-gateway-cert" → "api-gateway-cert_1672531200" +Step 4: Update client-ssl templates: cert "api-gateway-cert" → "api-gateway-cert_1672531200" +Step 5: Remove old certificate "api-gateway-cert" +Step 6: Rebind templates to virtual services +``` + + + +##### Alias Best Practices +- Use descriptive names that indicate purpose: `web-frontend-ssl`, `api-backend-tls` +- Avoid special characters that might conflict with A10 naming rules +- Consider including environment indicators: `prod-web-cert`, `stage-api-cert` +- Remember that renaming will append timestamps for template-bound certificates + + + +##### Character Limitations +- **Maximum Length**: 240 characters (enforced by orchestrator) +- **Recommended Characters**: Letters, numbers, hyphens, underscores +- **Avoid**: Special characters that might cause issues in API calls or file operations + +#### Troubleshooting Alias Issues + +##### ThunderSsl Common Issues +- **Template Update Failures**: Verify templates exist and are accessible +- **Long Alias Names**: Orchestrator will truncate to fit timestamp if needed +- **Special Characters**: May cause API call failures + + +#### Notes + +- This user will now be able to authenticate and perform actions via A10's AXAPI (v2/v3) interface. +- Role-Based Access (RBA) and partition assignment can further fine-tune access control. + +#### Example Login via AXAPI + +Example using `curl` for AXAPI v3 login: +```bash +curl -X POST https:///axapi/v3/auth \ + -d '{"credentials":{"username":"apiuser","password":"yourStrongPassword"}}' \ + -H "Content-Type: application/json" +``` + +## Certificate Store Configuration + +### ⚙️ Configuration Fields + +| Name | Display Name | Description | Type | Required | +|-------------------|-------------------------------|--------------------------------------------------------------|--------|----------| +| allowInvalidCert | Allow Invalid Cert on A10 API | If true, allows self-signed/untrusted certs for A10 API access | Bool | ✅ (default: true) | + diff --git a/images/TC1.gif b/images/TC1.gif new file mode 100644 index 0000000..1373f5e Binary files /dev/null and b/images/TC1.gif differ diff --git a/images/TC10.gif b/images/TC10.gif new file mode 100644 index 0000000..75dc77a Binary files /dev/null and b/images/TC10.gif differ diff --git a/images/TC11.gif b/images/TC11.gif new file mode 100644 index 0000000..d43c293 Binary files /dev/null and b/images/TC11.gif differ diff --git a/images/TC13.gif b/images/TC13.gif new file mode 100644 index 0000000..4ec2b50 Binary files /dev/null and b/images/TC13.gif differ diff --git a/images/TC14.gif b/images/TC14.gif new file mode 100644 index 0000000..847a5c2 Binary files /dev/null and b/images/TC14.gif differ diff --git a/images/TC2.gif b/images/TC2.gif new file mode 100644 index 0000000..80a12cc Binary files /dev/null and b/images/TC2.gif differ diff --git a/images/TC3.gif b/images/TC3.gif new file mode 100644 index 0000000..a4e2132 Binary files /dev/null and b/images/TC3.gif differ diff --git a/images/TC4.gif b/images/TC4.gif new file mode 100644 index 0000000..8c9e271 Binary files /dev/null and b/images/TC4.gif differ diff --git a/images/TC5.gif b/images/TC5.gif new file mode 100644 index 0000000..9b8a52e Binary files /dev/null and b/images/TC5.gif differ diff --git a/images/TC6.gif b/images/TC6.gif new file mode 100644 index 0000000..38c498c Binary files /dev/null and b/images/TC6.gif differ diff --git a/images/TC7.gif b/images/TC7.gif new file mode 100644 index 0000000..ce91a87 Binary files /dev/null and b/images/TC7.gif differ diff --git a/images/TC8.gif b/images/TC8.gif new file mode 100644 index 0000000..ba84fb4 Binary files /dev/null and b/images/TC8.gif differ diff --git a/images/TC9.gif b/images/TC9.gif new file mode 100644 index 0000000..09b657b Binary files /dev/null and b/images/TC9.gif differ diff --git a/integration-manifest.json b/integration-manifest.json index 57cf9cf..d0466f4 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -1,32 +1,142 @@ { - "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", - "integration_type": "orchestrator", - "name": "a10vThunder", - "status": "production", - "update_catalog": true, - "link_github": true, - "support_level": "kf-supported", - "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.", - "about": { - "orchestrator": { - "win": { - "supportsCreateStore": false, - "supportsDiscovery": false, - "supportsManagementAdd": true, - "supportsManagementRemove": true, - "supportsReenrollment": false, - "supportsInventory": true, - "platformSupport": "Unused" - }, - "linux": { - "supportsCreateStore": false, - "supportsDiscovery": false, - "supportsManagementAdd": true, - "supportsManagementRemove": true, - "supportsReenrollment": false, - "supportsInventory": true, - "platformSupport": "Unused" - } - } - } +"$schema": "https://keyfactor.github.io/integration-manifest-schema.json", +"integration_type": "orchestrator", +"name": "a10vThunder Orchestrator", +"status": "production", +"link_github": true, +"release_project": "a10vthunder-orchestrator/A10vThunder.csproj", +"release_dir": "a10vthunder-orchestrator/bin/Release", +"update_catalog": true, +"support_level": "kf-supported", +"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.", +"about": { +"orchestrator": { +"UOFramework": "10.4", +"pam_support": true, +"keyfactor_platform_version": "11.0", +"store_types": [ +{ +"Name": "A10 Thunder Ssl Certificates", +"ShortName": "ThunderSsl", +"Capability": "ThunderSsl", +"LocalStore": false, +"SupportedOperations": { +"Add": true, +"Create": false, +"Discovery": false, +"Enrollment": false, +"Remove": true +}, +"Properties": [ +{ + "Name": "allowInvalidCert", + "DisplayName": "Allow Invalid Cert on A10 Management API", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "true", + "Required": true, + "Description": "Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections." +} +], +"EntryParameters": [], +"PasswordOptions": { +"EntrySupported": false, +"StoreRequired": false, +"Style": "Default" +}, +"StorePathValue": "", +"ClientMachineDescription": "Hostname or IP address of the A10 vThunder appliance to be managed. The orchestrator will establish an AXAPI (REST API) connection using the credentials specified in the Server Username and Server Password fields to manage SSL certificates directly on the device.", +"StorePathDescription": "A10 partition name where certificates will be managed. Use 'shared' for the default shared partition, or specify a custom partition name (e.g., 'tenant-prod') for multi-tenant deployments. The partition must already exist on the A10 device. Leave empty to default to the shared partition.", +"PrivateKeyAllowed": "Optional", +"ServerRequired": true, +"PowerShell": false, +"BlueprintAllowed": false, +"CustomAliasAllowed": "Required" +}, +{ +"Name": "A10 Thunder Management Certificates", +"ShortName": "ThunderMgmt", +"Capability": "ThunderMgmt", +"LocalStore": false, +"SupportedOperations": { +"Add": true, +"Create": false, +"Discovery": false, +"Enrollment": false, +"Remove": true +}, +"Properties": [ +{ + "Name": "OrchToScpServerIp", + "DisplayName": "Orch To Scp Server Ip", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": true, + "Description": "IP address or hostname of the SCP server that the Universal Orchestrator will connect to for uploading certificate files. This SCP server acts as an intermediary storage location before the A10 device retrieves the certificates." +}, +{ + "Name": "ScpPort", + "DisplayName": "Port Used For Scp", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": true, + "Description": "TCP port number used for SSH/SCP connections to the SCP server. Typically port 22 for standard SSH/SCP operations." +}, +{ + "Name": "ScpUserName", + "DisplayName": "UserName Used For Scp", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": true, + "Description": "Username credential for authenticating to the SCP server. This account must have write permissions to the target directory path specified in the certificate store configuration. Supports PAM integration for secure credential retrieval." +}, +{ + "Name": "ScpPassword", + "DisplayName": "Password Used For Scp", + "Type": "Secret", + "DependsOn": "", + "DefaultValue": "", + "Required": true, + "Description": "Password credential for authenticating to the SCP server. Used in conjunction with ScpUserName for SSH/SCP authentication. Supports PAM integration for secure credential retrieval." +}, +{ + "Name": "A10ToScpServerIp", + "DisplayName": "A10 Device To Scp Server Ip", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": true, + "Description": "IP address or hostname that the A10 vThunder device uses to connect to the SCP server for retrieving certificate files. This may differ from OrchToScpServerIp due to network topology, routing, or firewall configurations where the A10 device and orchestrator access the SCP server through different network paths." +}, +{ + "Name": "allowInvalidCert", + "DisplayName": "Allow Invalid Cert on A10 Management API", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "true", + "Required": true, + "Description": "Boolean value specifying whether to allow connections to the A10 vThunder management API when it presents an invalid or self-signed SSL/TLS certificate. Set to true to bypass certificate validation for AXAPI connections used during the certificate installation process." +} +], +"EntryParameters": [], +"PasswordOptions": { +"EntrySupported": false, +"StoreRequired": false, +"Style": "Default" +}, +"StorePathValue": "", +"ClientMachineDescription": "Hostname or IP address of the A10 vThunder appliance to be managed. The orchestrator will establish an AXAPI (REST API) connection using the credentials specified in the Server Username and Server Password fields to trigger certificate installation on the management interface after uploading files via SCP.", +"StorePathDescription": "Absolute directory path on the SCP server where certificate files (.crt and .key) will be uploaded. The A10 device will retrieve certificate files from this location. Example: '/home/certuser'. The specified path must exist and the SCP user must have write permissions to this directory.", +"PrivateKeyAllowed": "Required", +"ServerRequired": true, +"PowerShell": false, +"BlueprintAllowed": false, +"CustomAliasAllowed": "Required" +} +] +} +} } diff --git a/readme_source.md b/readme_source.md index bf7b66b..2350458 100644 --- a/readme_source.md +++ b/readme_source.md @@ -82,62 +82,4 @@ 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 -