diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml new file mode 100644 index 0000000..6d8de53 --- /dev/null +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -0,0 +1,19 @@ +name: Keyfactor Bootstrap Workflow + +on: + workflow_dispatch: + pull_request: + types: [opened, closed, synchronize, edited, reopened] + push: + create: + branches: + - 'release-*.*' + +jobs: + call-starter-workflow: + uses: keyfactor/actions/.github/workflows/starter.yml@v2 + secrets: + token: ${{ secrets.V2BUILDTOKEN}} + APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfcfd56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,350 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1d2f874 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +v1.0.0 +- Initial Version \ No newline at end of file diff --git a/F5WafOrchestrator.Tests/F5WafCa.cs b/F5WafOrchestrator.Tests/F5WafCa.cs index 2fe2844..03ee7fe 100644 --- a/F5WafOrchestrator.Tests/F5WafCa.cs +++ b/F5WafOrchestrator.Tests/F5WafCa.cs @@ -1,4 +1,17 @@ -using System.Security.Cryptography.X509Certificates; +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + using Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.CA; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; diff --git a/F5WafOrchestrator.Tests/F5WafTls.cs b/F5WafOrchestrator.Tests/F5WafTls.cs index 21ca11c..79e292f 100644 --- a/F5WafOrchestrator.Tests/F5WafTls.cs +++ b/F5WafOrchestrator.Tests/F5WafTls.cs @@ -1,13 +1,23 @@ -using System.Security.Cryptography.X509Certificates; -using Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.Client; +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + using Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.TLS; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; using NLog.Extensions.Logging; -using Org.BouncyCastle.Pkcs; namespace F5WafOrchestrator.Tests; diff --git a/F5WafOrchestrator/CA/Discovery.cs b/F5WafOrchestrator/CA/Discovery.cs index 8c79e5f..1f52c0b 100644 --- a/F5WafOrchestrator/CA/Discovery.cs +++ b/F5WafOrchestrator/CA/Discovery.cs @@ -1,3 +1,17 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + using Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.Client; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; @@ -6,47 +20,33 @@ namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.CA; -[Job("Discovery")] -public class Discovery : Job, IDiscoveryJobExtension +public class Discovery : Job, IDiscoveryJobExtension { ILogger _logger = LogHandler.GetClassLogger(); - - public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate cb) + + public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate cb) + { + _logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}..."); + _logger.LogDebug($"Server: {config.ClientMachine}"); + + List namespaces; + + try { - _logger.LogDebug("Beginning F5 Distributed Cloud Discovery Job"); - - var result = new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId - }; - - try - { - F5Client = new F5WafClient(config.ClientMachine, config.ServerPassword); - } catch (Exception ex) - { - _logger.LogError(ex, $"Could not connect to F5 Client" + ex.Message); - return result; - } - - List namespaces; - - try - { - namespaces = F5Client.DiscoverNamespacesforCaStoreType(); - _logger.LogDebug($"Found {namespaces.Count()} namespaces in {config.ClientMachine}"); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error getting F5 namespaces from {config.ClientMachine}:\n" + ex.Message); - result.FailureMessage = $"Error getting F5 namespaces from {config.ClientMachine}:\n" + ex.Message; - return result; - } - - cb.Invoke(namespaces); - - result.Result = OrchestratorJobStatusJobResult.Success; - return result; + F5Client = new F5WafClient(config.ClientMachine, config.ServerPassword); + + namespaces = F5Client.DiscoverNamespacesforStoreType("ca-"); + _logger.LogDebug($"Found {namespaces.Count()} namespaces in {config.ClientMachine}"); } + catch (Exception ex) + { + _logger.LogError($"Exception for {config.Capability}: {F5WAFException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = F5WAFException.FlattenExceptionMessages(ex, $"Server {config.ClientMachine}:") }; + } + + cb.Invoke(namespaces); + + _logger.LogDebug($"...End {config.Capability} job for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; + } } \ No newline at end of file diff --git a/F5WafOrchestrator/CA/Inventory.cs b/F5WafOrchestrator/CA/Inventory.cs index 09e7b93..7a4a388 100644 --- a/F5WafOrchestrator/CA/Inventory.cs +++ b/F5WafOrchestrator/CA/Inventory.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 Keyfactor // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,37 +20,26 @@ namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.CA; -[Job("Inventory")] -public class Inventory : Job, IInventoryJobExtension +public class Inventory : Job, IInventoryJobExtension { private readonly ILogger _logger = LogHandler.GetClassLogger(); public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate cb) { - _logger.LogDebug("Beginning F5 Distributed Cloud Inventory Job"); - - var result = new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId - }; - + _logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}..."); + _logger.LogDebug($"Server: {config.CertificateStoreDetails.ClientMachine}"); + _logger.LogDebug($"Store Path: {config.CertificateStoreDetails.StorePath}"); + + List inventoryItems; + try { F5Client = new F5WafClient(config.CertificateStoreDetails.ClientMachine, config.ServerPassword); - } catch (Exception ex) - { - _logger.LogError(ex, $"Could not connect to F5 Client" + ex.Message); - return result; - } - - List inventoryItems; - string storePath = config.CertificateStoreDetails.StorePath; + string storePath = config.CertificateStoreDetails.StorePath; - try - { - // check if the string starts with "ca-" and remove it if present + // check if the string starts with "ca-" and remove it if present. This may occur + // if store was created in Command via a Discovery job if (config.CertificateStoreDetails.StorePath.StartsWith("ca-")) { storePath = config.CertificateStoreDetails.StorePath.Substring(3); // Skip the first 3 characters ("ca-") @@ -68,14 +57,21 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd } catch (Exception ex) { - _logger.LogError(ex, $"Error getting F5 Certificate from namespace {storePath}:\n" + ex.Message); - result.FailureMessage = $"Error getting F5 Certificates from namespace {storePath}:\n" + ex.Message; - return result; + _logger.LogError($"Exception for {config.Capability}: {F5WAFException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = F5WAFException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}:") }; } - - cb.Invoke(inventoryItems); - result.Result = OrchestratorJobStatusJobResult.Success; - return result; + try + { + cb.Invoke(inventoryItems); + _logger.LogDebug($"...End {config.Capability} job for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; + } + catch (Exception ex) + { + string errorMessage = F5WAFException.FlattenExceptionMessages(ex, string.Empty); + _logger.LogError($"Exception returning certificates for {config.Capability}: {errorMessage} for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = F5WAFException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}:") }; + } } } \ No newline at end of file diff --git a/F5WafOrchestrator/CA/Management.cs b/F5WafOrchestrator/CA/Management.cs index c496c5a..43db82b 100644 --- a/F5WafOrchestrator/CA/Management.cs +++ b/F5WafOrchestrator/CA/Management.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 Keyfactor // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,105 +20,80 @@ namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.CA; -[Job("Management")] -public class Management : Job, IManagementJobExtension +public class Management : Job, IManagementJobExtension { ILogger _logger = LogHandler.GetClassLogger(); - public JobResult ProcessJob(ManagementJobConfiguration config) + public JobResult ProcessJob(ManagementJobConfiguration config) + { + _logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}..."); + _logger.LogDebug($"Server: {config.CertificateStoreDetails.ClientMachine}"); + _logger.LogDebug($"Store Path: {config.CertificateStoreDetails.StorePath}"); + + try { - _logger.LogDebug("Beginning F5 Cloud Management Job"); + F5Client = new F5WafClient(config.CertificateStoreDetails.ClientMachine, config.ServerPassword); - JobResult result = new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId - }; - - try + // check if the string starts with "ca-" and remove it if present. This may occur + // if store was created in Command via a Discovery job + if (config.CertificateStoreDetails.StorePath.StartsWith("ca-")) { - F5Client = new F5WafClient(config.CertificateStoreDetails.ClientMachine, config.ServerPassword); - } catch (Exception ex) + config.CertificateStoreDetails.StorePath = config.CertificateStoreDetails.StorePath.Substring(3); + } + + switch (config.OperationType) { - _logger.LogError(ex, $"Could not connect to F5 Client" + ex.Message); - return result; + case CertStoreOperationType.Add: + _logger.LogDebug($"BEGIN Add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}."); + PerformCaCertAddition(config); + _logger.LogDebug($"END Add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}."); + break; + case CertStoreOperationType.Remove: + _logger.LogDebug($"BEGIN Delete Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}."); + F5Client.RemoveCaOrTlsCertificate(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias, false); + _logger.LogDebug($"END Delete Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}."); + break; + default: + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Unsupported operation: {config.OperationType.ToString()}" }; } + } + catch (Exception ex) + { + _logger.LogError($"Exception for {config.Capability}: {F5WAFException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = F5WAFException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}:") }; + } - try - { - // check if the string starts with "ca-" and remove it if present - if (config.CertificateStoreDetails.StorePath.StartsWith("ca-")) - { - config.CertificateStoreDetails.StorePath = config.CertificateStoreDetails.StorePath.Substring(3); - } - - switch (config.OperationType) - { - case CertStoreOperationType.Add: - _logger.LogDebug("Adding certificate to F5 Cloud"); - - PerformCaCertAddition(config, result); + _logger.LogDebug($"...End {config.Capability} job for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; + } - _logger.LogDebug("Add operation complete."); - - result.Result = OrchestratorJobStatusJobResult.Success; - break; - case CertStoreOperationType.Remove: - _logger.LogDebug("Removing certificate from F5 Cloud"); - - F5Client.RemoveCaCertificate(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias); + private void PerformCaCertAddition(ManagementJobConfiguration config) + { + _logger.MethodEntry(LogLevel.Debug); - _logger.LogDebug("Remove operation complete."); - - result.Result = OrchestratorJobStatusJobResult.Success; - break; - default: - _logger.LogDebug("Invalid management operation type: {0}", config.OperationType); - throw new ArgumentOutOfRangeException(); - } - } catch (Exception ex) - { - _logger.LogError(ex, "Error processing job:\n {0}", ex.Message); - result.FailureMessage = ex.Message; - return result; - } + bool certificateExists = F5Client.CertificateExistsInF5(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias, false); - return result; - } - - private void PerformCaCertAddition(ManagementJobConfiguration config, JobResult result) + if (certificateExists && !config.Overwrite) { - // ensure that an alias is provided - if (string.IsNullOrWhiteSpace(config.JobCertificate.Alias)) - { - throw new Exception("Certificate alias is required."); - } - // ensure that an alias is provided - if (!string.IsNullOrWhiteSpace(config.JobCertificate.PrivateKeyPassword)) - { - throw new Exception("Certificate passed is not a CA."); - } - - if (F5Client.CertificateExistsInF5(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias) && !config.Overwrite) - { - string message = - $"Certificate with alias \"{config.JobCertificate.Alias}\" already exists in F5, and job was not configured to overwrite."; - _logger.LogDebug(message); - throw new Exception(message); - } + string message = + $"Certificate with alias \"{config.JobCertificate.Alias}\" already exists in F5, and job was not configured to overwrite."; + _logger.LogDebug(message); + throw new Exception(message); + } - F5WafClient.CaPostRoot reqBody = F5Client.FormatCaCertificateRequest(config.JobCertificate); - if (F5Client.CertificateExistsInF5(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias) && - config.Overwrite) - { - _logger.LogDebug("Overwrite is enabled, replacing certificate in F5 called \"{0}\"", - config.JobCertificate.Alias); - F5Client.ReplaceCaCertificateInF5(config.CertificateStoreDetails.StorePath, reqBody); - } - else - { - _logger.LogDebug("Adding certificate to F5 Cloud"); - F5Client.AddCaCertificate(config.CertificateStoreDetails.StorePath, reqBody); - } + F5WafClient.CaPostRoot reqBody = F5Client.FormatCaCertificateRequest(config.JobCertificate); + if (certificateExists && config.Overwrite) + { + _logger.LogDebug("Overwrite is enabled, replacing certificate in F5 called \"{0}\"", + config.JobCertificate.Alias); + F5Client.ReplaceCaOrTlsCertificate(config.CertificateStoreDetails.StorePath, null, reqBody, false); } + else + { + _logger.LogDebug("Adding certificate to F5 Cloud"); + F5Client.AddCaOrTlsCertificate(config.CertificateStoreDetails.StorePath, null, reqBody, false); + } + + _logger.MethodExit(LogLevel.Debug); + } } diff --git a/F5WafOrchestrator/F5WAFExceptionHandler.cs b/F5WafOrchestrator/F5WAFExceptionHandler.cs new file mode 100644 index 0000000..869d3c7 --- /dev/null +++ b/F5WafOrchestrator/F5WAFExceptionHandler.cs @@ -0,0 +1,36 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator +{ + class F5WAFException : ApplicationException + { + public F5WAFException(string? message) : base(message) + { } + + public F5WAFException(string message, Exception ex) : base(message, ex) + { } + + public static string FlattenExceptionMessages(Exception ex, string message) + { + message += ex.Message + Environment.NewLine; + if (ex.InnerException != null) + message = FlattenExceptionMessages(ex.InnerException, message); + + return message; + } + } +} diff --git a/F5WafOrchestrator/F5WafClient.cs b/F5WafOrchestrator/F5WafClient.cs index dfdb271..f017abe 100644 --- a/F5WafOrchestrator/F5WafClient.cs +++ b/F5WafOrchestrator/F5WafClient.cs @@ -1,3 +1,17 @@ +// Copyright 2024 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.Net.Http.Headers; using System.Text; using System.Text.Json; @@ -9,7 +23,9 @@ using Keyfactor.Orchestrators.Extensions; using Newtonsoft.Json.Linq; using Org.BouncyCastle.Pkcs; - +using System.Net; +using Org.BouncyCastle.Asn1.Ocsp; +using System.Xml.Linq; namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.Client; @@ -105,193 +121,120 @@ public class CaRootObject public CaSpec? spec { get; set; } } - public F5WafClient(string hostname, string apiToken) - { - Log = LogHandler.GetClassLogger(); - Log.LogDebug("Initializing F5 Distributed Cloud client"); - - var f5ClientHandler = new HttpClientHandler(); - - F5Client = new HttpClient(f5ClientHandler); - F5Client.BaseAddress = new Uri("https://" + hostname); - - var auth = $"APIToken {apiToken}"; - F5Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", auth); - } + private string HostName { get; set; } - private ILogger Log { get; } private HttpClient F5Client { get; } - public string GetTlsCertificatesFromF5(string f5Namespace) + private ILogger _logger { get; } + + internal F5WafClient(string hostname, string apiToken) { - var response = F5Client.GetAsync($"/api/config/namespaces/{f5Namespace}/certificates"); - response.Wait(); - var stringResponse = response.Result.Content.ReadAsStringAsync(); - stringResponse.Wait(); - - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - throw new Exception(stringResponse.ToString()); - } + _logger = LogHandler.GetClassLogger(); + _logger.MethodEntry(LogLevel.Debug); + _logger.LogDebug("Initializing F5 Distributed Cloud client"); + + HostName = "https://" + hostname; - return stringResponse.Result; + F5Client = new HttpClient(new HttpClientHandler()); + F5Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", $"APIToken {apiToken}"); + + _logger.MethodExit(LogLevel.Debug); + } + + internal string GetTlsCertificatesFromF5(string f5Namespace) + { + _logger.MethodEntry(LogLevel.Debug); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{HostName}/api/config/namespaces/{f5Namespace}/certificates"); + string result = SubmitRequest(request); + + _logger.MethodExit(LogLevel.Debug); + + return result; } - public string GetCaCertificatesFromF5(string f5Namespace) + internal string GetCaCertificatesFromF5(string f5Namespace) { - var response = F5Client.GetAsync($"/api/config/namespaces/{f5Namespace}/trusted_ca_lists"); - response.Wait(); - var stringResponse = response.Result.Content.ReadAsStringAsync(); - stringResponse.Wait(); - - // parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - throw new Exception(stringResponse.ToString()); - } - - return stringResponse.Result; + _logger.MethodEntry(LogLevel.Debug); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{HostName}/api/config/namespaces/{f5Namespace}/trusted_ca_lists"); + string result = SubmitRequest(request); + + _logger.MethodExit(LogLevel.Debug); + + return result; } - public string GetHttpLoadBalancersFromF5(string f5Namespace) + internal string GetHttpLoadBalancersFromF5(string f5Namespace) { - var response = F5Client.GetAsync($"/api/config/namespaces/{f5Namespace}/http_loadbalancers"); - response.Wait(); - var stringResponse = response.Result.Content.ReadAsStringAsync(); - stringResponse.Wait(); - - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - throw new Exception(stringResponse.ToString()); - } - - return stringResponse.Result; + _logger.MethodEntry(LogLevel.Debug); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{HostName}/api/config/namespaces/{f5Namespace}/http_loadbalancers"); + string result = SubmitRequest(request); + + _logger.MethodExit(LogLevel.Debug); + + return result; } - public string GetHttpLoadBalancerFromF5(string f5Namespace, string certAlias) + internal string GetHttpLoadBalancerFromF5(string f5Namespace, string certAlias) { - var response = F5Client.GetAsync($"/api/config/namespaces/{f5Namespace}/http_loadbalancers/{certAlias}?response_format=GET_RSP_FORMAT_DEFAULT"); - response.Wait(); - var stringResponse = response.Result.Content.ReadAsStringAsync(); - stringResponse.Wait(); - - // parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - throw new Exception(stringResponse.ToString()); - } - - return stringResponse.Result; + _logger.MethodEntry(LogLevel.Debug); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{HostName}/api/config/namespaces/{f5Namespace}/http_loadbalancers/{certAlias}?response_format=GET_RSP_FORMAT_DEFAULT"); + string result = SubmitRequest(request); + + _logger.MethodExit(LogLevel.Debug); + + return result; } - public string? GetTlsCertificateContentsFromF5(string f5Namespace, string certName) + internal string? GetTlsCertificateContentsFromF5(string f5Namespace, string certName) { - var response = F5Client.GetAsync($"/api/config/namespaces/{f5Namespace}/certificates/{certName}?response_format=GET_RSP_FORMAT_DEFAULT"); - response.Wait(); - var resp = response.Result.Content.ReadAsStringAsync(); - resp.Wait(); + _logger.MethodEntry(LogLevel.Debug); - // parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - throw new Exception($"Error retrieving F5 certificate contents: {resp}"); - } - - RootObject rootObject = JsonSerializer.Deserialize(resp.Result) + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{HostName}/api/config/namespaces/{f5Namespace}/certificates/{certName}?response_format=GET_RSP_FORMAT_DEFAULT"); + string result = SubmitRequest(request); + + RootObject rootObject = JsonSerializer.Deserialize(result) ?? throw new InvalidOperationException("Deserialized RootObject is null."); - + + _logger.MethodExit(LogLevel.Debug); + return rootObject.spec?.certificate_url; } - public string? GetCaCertificateContentsFromF5(string f5Namespace, string certName) + internal string? GetCaCertificateContentsFromF5(string f5Namespace, string certName) { - var response = F5Client.GetAsync($"/api/config/namespaces/{f5Namespace}/trusted_ca_lists/{certName}?response_format=GET_RSP_FORMAT_DEFAULT"); - response.Wait(); - var resp = response.Result.Content.ReadAsStringAsync(); - resp.Wait(); + _logger.MethodEntry(LogLevel.Debug); - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - throw new Exception($"Error retrieving F5 certificate contents: {resp}"); - } - - CaRootObject rootObject = JsonSerializer.Deserialize(resp.Result) + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{HostName}/api/config/namespaces/{f5Namespace}/trusted_ca_lists/{certName}?response_format=GET_RSP_FORMAT_DEFAULT"); + string result = SubmitRequest(request); + + CaRootObject rootObject = JsonSerializer.Deserialize(result) ?? throw new InvalidOperationException("Deserialized RootObject is null."); - + + _logger.MethodExit(LogLevel.Debug); + return rootObject.spec?.trusted_ca_url; } - - public (IEnumerable, IEnumerable) TlsCertificateRetrievalProcess(string f5Namespace) + + internal string GetNamespaces() + { + _logger.MethodEntry(LogLevel.Debug); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{HostName}/api/web/namespaces"); + string result = SubmitRequest(request); + + _logger.MethodExit(LogLevel.Debug); + + return result; + } + + internal (IEnumerable, IEnumerable) TlsCertificateRetrievalProcess(string f5Namespace) { + _logger.MethodEntry(LogLevel.Debug); + List encodedCerts = new List(); List certNames = new List(); try @@ -321,7 +264,7 @@ public string GetHttpLoadBalancerFromF5(string f5Namespace, string certAlias) } else { - Log.LogDebug($"No certificates found in F5."); + _logger.LogDebug($"No certificates found in F5."); } } } @@ -329,15 +272,19 @@ public string GetHttpLoadBalancerFromF5(string f5Namespace, string certAlias) } catch (Exception ex) { - Log.LogDebug($"Error retrieving F5 certificates: {ex.Message}"); - throw new Exception($"Error retrieving F5 certificates: {ex.Message}"); + _logger.LogDebug($"Error retrieving F5 certificates: {ex.Message}"); + throw new F5WAFException($"Error retrieving F5 certificates: {ex.Message}"); } - + + _logger.MethodExit(LogLevel.Debug); + return (certNames, encodedCerts); } - public (IEnumerable, IEnumerable) CaCertificateRetrievalProcess(string f5Namespace) + internal (IEnumerable, IEnumerable) CaCertificateRetrievalProcess(string f5Namespace) { + _logger.MethodEntry(LogLevel.Debug); + List encodedCerts = new List(); List certNames = new List(); try @@ -367,7 +314,7 @@ public string GetHttpLoadBalancerFromF5(string f5Namespace, string certAlias) } else { - Log.LogDebug($"No certificates found in F5."); + _logger.LogDebug($"No certificates found in F5."); } } } @@ -375,135 +322,42 @@ public string GetHttpLoadBalancerFromF5(string f5Namespace, string certAlias) } catch (Exception ex) { - Log.LogDebug($"Error retrieving F5 certificates: {ex.Message}"); - throw new Exception($"Error retrieving F5 certificates: {ex.Message}"); - } - - return (certNames, encodedCerts); - } - - static string ConvertCertToPemFormat(string base64EncodedCertificate) - { - StringBuilder builder = new StringBuilder(); - - builder.Append("-----BEGIN CERTIFICATE-----\n"); - - // split base64 string into 64-character lines - for (int i = 0; i < base64EncodedCertificate.Length; i += 64) - { - int lineLength = Math.Min(64, base64EncodedCertificate.Length - i); - string line = base64EncodedCertificate.Substring(i, lineLength); - builder.Append(line + "\n"); + _logger.LogDebug($"Error retrieving F5 certificates: {ex.Message}"); + throw new F5WAFException($"Error retrieving F5 certificates: {ex.Message}"); } - builder.Append("-----END CERTIFICATE-----\n"); + _logger.MethodExit(LogLevel.Debug); - return builder.ToString(); + return (certNames, encodedCerts); } - - static string ConvertKeyToPemFormat(string base64EncodedCertificate) + + internal void AddCaOrTlsCertificate(string f5Namespace, PostRoot? tlsReqBody, CaPostRoot? caReqBody, bool isTLSCertificate) { - StringBuilder builder = new StringBuilder(); + _logger.MethodEntry(LogLevel.Debug); - builder.Append("-----BEGIN RSA PRIVATE KEY-----\n"); + string certType = "trusted_ca_lists"; + var jsonReqBody = JsonSerializer.Serialize(caReqBody); - // split base64 string into 64-character lines - for (int i = 0; i < base64EncodedCertificate.Length; i += 64) + if (isTLSCertificate) { - int lineLength = Math.Min(64, base64EncodedCertificate.Length - i); - string line = base64EncodedCertificate.Substring(i, lineLength); - builder.Append(line + "\n"); + certType = "certificates"; + jsonReqBody = JsonSerializer.Serialize(tlsReqBody); } - builder.Append("-----END RSA PRIVATE KEY-----\n"); - - return builder.ToString(); - } - - public string ExtractEndEntityandCertChain(string pfxData, string password) - { - string endEntityandChain = ""; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, $"{HostName}/api/config/namespaces/{f5Namespace}/{certType}"); - byte[] pfxBytes = Convert.FromBase64String(pfxData); - Pkcs12Store store = new Pkcs12Store(new MemoryStream(pfxBytes), password.ToCharArray()); - foreach (string alias in store.Aliases) - { - if (store.IsKeyEntry(alias)) - { - X509CertificateEntry[] chain = store.GetCertificateChain(alias); - if (chain == null) - { - throw new Exception("No certificate chain found or no key entry exists."); - } - string[] pemCertificates = new string[chain.Length]; - for (int i = 0; i < chain.Length; i++) - { - pemCertificates[i] = ConvertCertToPemFormat(Convert.ToBase64String(chain[i].Certificate.GetEncoded())); - endEntityandChain += pemCertificates[i]; - } - } - } - return endEntityandChain; - } - - public void AddTlsCertificate(string f5Namespace, PostRoot reqBody) - { - var jsonReqBody = JsonSerializer.Serialize(reqBody); var stringReqBody = new StringContent(jsonReqBody, Encoding.UTF8, "application/json"); + request.Content = stringReqBody; + + SubmitRequest(request); - var response = F5Client.PostAsync($"/api/config/namespaces/{f5Namespace}/certificates", stringReqBody); - response.Wait(); - - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - var errorMessage = response.Result.Content.ReadAsStringAsync(); - errorMessage.Wait(); - throw new Exception(errorMessage.ToString()); - } - } - - public void AddCaCertificate(string f5Namespace, CaPostRoot reqBody) - { - var jsonReqBody = JsonSerializer.Serialize(reqBody); - var stringReqBody = new StringContent(jsonReqBody, Encoding.UTF8, "application/json"); - - var response = F5Client.PostAsync($"/api/config/namespaces/{f5Namespace}/trusted_ca_lists", stringReqBody); - response.Wait(); - - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - var errorMessage = response.Result.Content.ReadAsStringAsync(); - errorMessage.Wait(); - throw new Exception(errorMessage.ToString()); - } + _logger.MethodExit(LogLevel.Debug); } - public PostRoot FormatTlsCertificateRequest(ManagementJobCertificate mgmtJobCert) + internal PostRoot FormatTlsCertificateRequest(ManagementJobCertificate mgmtJobCert) { + _logger.MethodEntry(LogLevel.Debug); + X509Certificate2 certX509; try { @@ -568,11 +422,16 @@ public PostRoot FormatTlsCertificateRequest(ManagementJobCertificate mgmtJobCert DisableOcspStapling = new DisableOcspStapling() } }; + + _logger.MethodExit(LogLevel.Debug); + return reqBody; } - public CaPostRoot FormatCaCertificateRequest(ManagementJobCertificate mgmtJobCert) + internal CaPostRoot FormatCaCertificateRequest(ManagementJobCertificate mgmtJobCert) { + _logger.MethodEntry(LogLevel.Debug); + X509Certificate2 certX509; try { @@ -608,68 +467,32 @@ public CaPostRoot FormatCaCertificateRequest(ManagementJobCertificate mgmtJobCer trusted_ca_url = "string:///" + encodedCertUtf8Base64 } }; + + _logger.MethodExit(LogLevel.Debug); + return reqBody; } - public void RemoveTlsCertificate(string f5Namespace, string certName) - { - var response = F5Client.DeleteAsync($"/api/config/namespaces/{f5Namespace}/certificates/{certName}"); - response.Wait(); - var stringResponse = response.Result.Content.ReadAsStringAsync(); - stringResponse.Wait(); - - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - var errorMessage = response.Result.Content.ReadAsStringAsync(); - errorMessage.Wait(); - throw new Exception(errorMessage.ToString()); - } - } - - public void RemoveCaCertificate(string f5Namespace, string certName) + internal void RemoveCaOrTlsCertificate(string f5Namespace, string certName, bool isTLSCertificate) { - var response = F5Client.DeleteAsync($"/api/config/namespaces/{f5Namespace}/trusted_ca_lists/{certName}"); - response.Wait(); - var stringResponse = response.Result.Content.ReadAsStringAsync(); - stringResponse.Wait(); - - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - var errorMessage = response.Result.Content.ReadAsStringAsync(); - errorMessage.Wait(); - throw new Exception(errorMessage.ToString()); - } + _logger.MethodEntry(LogLevel.Debug); + + string certType = isTLSCertificate ? "certificates" : "trusted_ca_lists"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, $"{HostName}/api/config/namespaces/{f5Namespace}/{certType}/{certName}"); + string result = SubmitRequest(request); + + _logger.MethodExit(LogLevel.Debug); } - - public bool CertificateExistsInF5(string f5Namespace, string alias) + + internal bool CertificateExistsInF5(string f5Namespace, string alias, bool isTLSCertificate) { - var certsJson = GetTlsCertificatesFromF5(f5Namespace); + _logger.MethodEntry(LogLevel.Debug); + + var certsJson = isTLSCertificate ? GetTlsCertificatesFromF5(f5Namespace) : GetCaCertificatesFromF5(f5Namespace); var certs = JsonDocument.Parse(certsJson); - + + _logger.MethodExit(LogLevel.Debug); + // Iterate through the names of the cert items and return true if a name matching the alias exists return certs.RootElement .GetProperty("items") @@ -678,127 +501,35 @@ public bool CertificateExistsInF5(string f5Namespace, string alias) nameElement.GetString() == alias); } - public void ReplaceTlsCertificate(string f5Namespace, PostRoot reqBody) + internal void ReplaceCaOrTlsCertificate(string f5Namespace, PostRoot? tlsReqBody, CaPostRoot? caReqBody, bool isTLSCertificate) { - var jsonReqBody = JsonSerializer.Serialize(reqBody); - var stringReqBody = new StringContent(jsonReqBody, Encoding.UTF8, "application/json"); + _logger.MethodEntry(LogLevel.Debug); - var response = F5Client.PutAsync($"/api/config/namespaces/{f5Namespace}/certificates/{reqBody.Metadata.Name}", stringReqBody); - response.Wait(); + string certType = "trusted_ca_lists"; + string? metadataName = caReqBody?.Metadata.Name; + var jsonReqBody = JsonSerializer.Serialize(caReqBody); - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) + if (isTLSCertificate) { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - var errorMessage = response.Result.Content.ReadAsStringAsync(); - errorMessage.Wait(); - throw new Exception(errorMessage.ToString()); + certType = "certificates"; + metadataName = tlsReqBody?.Metadata.Name; + jsonReqBody = JsonSerializer.Serialize(tlsReqBody); } - } - - public void ReplaceCaCertificateInF5(string f5Namespace, CaPostRoot reqBody) - { - var jsonReqBody = JsonSerializer.Serialize(reqBody); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, $"{HostName}/api/config/namespaces/{f5Namespace}/{certType}/{metadataName}"); + var stringReqBody = new StringContent(jsonReqBody, Encoding.UTF8, "application/json"); + request.Content = stringReqBody; - var response = F5Client.PutAsync($"/api/config/namespaces/{f5Namespace}/trusted_ca_lists/{reqBody.Metadata.Name}", stringReqBody); - response.Wait(); + SubmitRequest(request); - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - var errorMessage = response.Result.Content.ReadAsStringAsync(); - errorMessage.Wait(); - throw new Exception(errorMessage.ToString()); - } + _logger.MethodExit(LogLevel.Debug); } - public string GetNamespaces() + internal List DiscoverNamespacesforStoreType(string storeTypePrefix) { - var response = F5Client.GetAsync($"/api/web/namespaces"); - response.Wait(); - var resp = response.Result.Content.ReadAsStringAsync(); - resp.Wait(); + _logger.MethodEntry(LogLevel.Debug); - //parse status code for error handling - string statusCode = string.Empty; - string[] respMessage = response.Result.ToString().Split(','); - for (int i = 0; i < respMessage.Length; i++) - { - if (respMessage[i].Contains("StatusCode:")) - { - statusCode = respMessage[i].Trim().Substring("StatsCode: ".Length).Trim(); - break; - } - } - - if (statusCode != "200") - { - throw new Exception($"Error retrieving F5 certificate contents: {resp}"); - } - - return resp.Result; - } - - public List DiscoverNamespacesforCaStoreType() - { - List namespacesList = new List(); - try - { - var namespacesJson = GetNamespaces(); - var namespaces = JsonDocument.Parse(namespacesJson); - - var items = namespaces.RootElement.GetProperty("items").EnumerateArray(); - - // Iterate through each cert in "items" JSON object - foreach (var item in items) - { - if (item.TryGetProperty("name", out JsonElement nameElement)) - { - string? name = nameElement.GetString(); - if (name != null) - { - namespacesList.Add("ca-" + name); - } - else - { - Log.LogDebug($"No namespaces found in F5."); - } - } - } - } - catch (Exception ex) - { - Log.LogDebug($"Error retrieving F5 namespaces: {ex.Message}"); - throw new Exception($"Error retrieving F5 namespaces: {ex.Message}"); - } - - return namespacesList; - } - - public List DiscoverNamespacesforTlsStoreType() - { List namespacesList = new List(); try { @@ -815,26 +546,26 @@ public List DiscoverNamespacesforTlsStoreType() string? name = nameElement.GetString(); if (name != null) { - namespacesList.Add("tls-" + name); - } - else - { - Log.LogDebug($"No namespaces found in F5."); + namespacesList.Add(storeTypePrefix + name); } } } } catch (Exception ex) { - Log.LogDebug($"Error retrieving F5 namespaces: {ex.Message}"); - throw new Exception($"Error retrieving F5 namespaces: {ex.Message}"); + _logger.LogDebug($"Error retrieving F5 namespaces: {ex.Message}"); + throw new F5WAFException($"Error retrieving F5 namespaces: {ex.Message}"); } - + + _logger.MethodExit(LogLevel.Debug); + return namespacesList; } - public bool JobCertIsAttachedToHttpLoadBalancer(string f5Namespace, string jobCertName) + internal bool JobCertIsAttachedToHttpLoadBalancer(string f5Namespace, string jobCertName) { + _logger.MethodEntry(LogLevel.Debug); + var certsJson = GetHttpLoadBalancersFromF5(f5Namespace); var certs = JsonDocument.Parse(certsJson); var items = certs.RootElement.GetProperty("items").EnumerateArray(); @@ -863,7 +594,7 @@ public bool JobCertIsAttachedToHttpLoadBalancer(string f5Namespace, string jobCe // check to see if job cert name matches certs tied to load balancer if (certificate != null) { - string certificateName = certificate["name"].ToString(); + string? certificateName = certificate["name"]?.ToString(); if (certificateName == jobCertName) { return true; @@ -874,6 +605,113 @@ public bool JobCertIsAttachedToHttpLoadBalancer(string f5Namespace, string jobCe } } } + + _logger.MethodExit(LogLevel.Debug); + return false; } + + private string SubmitRequest(HttpRequestMessage request) + { + _logger.MethodEntry(LogLevel.Debug); + + var response = F5Client.SendAsync(request).Result; + var result = response.Content.ReadAsStringAsync().Result; + + if (response.StatusCode != HttpStatusCode.OK && + response.StatusCode != HttpStatusCode.Accepted && + response.StatusCode != HttpStatusCode.Created && + response.StatusCode != HttpStatusCode.NoContent) + { + string errorMessage = $"Error calling {request.RequestUri}: {result}"; + _logger.LogError(errorMessage); + _logger.MethodExit(LogLevel.Debug); + throw new F5WAFException(errorMessage); + } + + _logger.MethodExit(LogLevel.Debug); + + return result; + } + + private string ExtractEndEntityandCertChain(string pfxData, string password) + { + _logger.MethodEntry(LogLevel.Debug); + + string endEntityandChain = ""; + + byte[] pfxBytes = Convert.FromBase64String(pfxData); + + Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder(); + Pkcs12Store store = storeBuilder.Build(); + store.Load(new MemoryStream(pfxBytes), password.ToCharArray()); + + foreach (string alias in store.Aliases) + { + if (store.IsKeyEntry(alias)) + { + X509CertificateEntry[] chain = store.GetCertificateChain(alias); + if (chain == null) + { + throw new F5WAFException("No certificate chain found or no key entry exists."); + } + string[] pemCertificates = new string[chain.Length]; + for (int i = 0; i < chain.Length; i++) + { + pemCertificates[i] = ConvertCertToPemFormat(Convert.ToBase64String(chain[i].Certificate.GetEncoded())); + endEntityandChain += pemCertificates[i]; + } + } + } + + _logger.MethodExit(LogLevel.Debug); + + return endEntityandChain; + } + + private string ConvertCertToPemFormat(string base64EncodedCertificate) + { + _logger.MethodEntry(LogLevel.Debug); + + StringBuilder builder = new StringBuilder(); + + builder.Append("-----BEGIN CERTIFICATE-----\n"); + + // split base64 string into 64-character lines + for (int i = 0; i < base64EncodedCertificate.Length; i += 64) + { + int lineLength = Math.Min(64, base64EncodedCertificate.Length - i); + string line = base64EncodedCertificate.Substring(i, lineLength); + builder.Append(line + "\n"); + } + + builder.Append("-----END CERTIFICATE-----\n"); + + _logger.MethodExit(LogLevel.Debug); + + return builder.ToString(); + } + + private string ConvertKeyToPemFormat(string base64EncodedCertificate) + { + _logger.MethodEntry(LogLevel.Debug); + + StringBuilder builder = new StringBuilder(); + + builder.Append("-----BEGIN RSA PRIVATE KEY-----\n"); + + // split base64 string into 64-character lines + for (int i = 0; i < base64EncodedCertificate.Length; i += 64) + { + int lineLength = Math.Min(64, base64EncodedCertificate.Length - i); + string line = base64EncodedCertificate.Substring(i, lineLength); + builder.Append(line + "\n"); + } + + builder.Append("-----END RSA PRIVATE KEY-----\n"); + + _logger.MethodExit(LogLevel.Debug); + + return builder.ToString(); + } } \ No newline at end of file diff --git a/F5WafOrchestrator/F5WafJob.cs b/F5WafOrchestrator/F5WafJob.cs index cb8f35e..9138bb4 100644 --- a/F5WafOrchestrator/F5WafJob.cs +++ b/F5WafOrchestrator/F5WafJob.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 Keyfactor // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator; -public abstract class Job : IOrchestratorJobExtension +public abstract class Job { - protected F5WafClient F5Client { get; set; } - public string ExtensionName => "F5Waf"; + internal F5WafClient F5Client { get; set; } + public string ExtensionName => ""; } \ No newline at end of file diff --git a/F5WafOrchestrator/F5WafOrchestrator.csproj b/F5WafOrchestrator/F5WafOrchestrator.csproj index c3ea971..e2ce2df 100644 --- a/F5WafOrchestrator/F5WafOrchestrator.csproj +++ b/F5WafOrchestrator/F5WafOrchestrator.csproj @@ -1,7 +1,9 @@ + false net6.0 + true enable enable Keyfactor.Extensions.Orchestrator.F5WafOrchestrator @@ -9,6 +11,7 @@ + @@ -32,11 +35,15 @@ - C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\F5WafOrchestrator + $(BaseOutputPath) false false + + + + diff --git a/F5WafOrchestrator/JobAttribute.cs b/F5WafOrchestrator/JobAttribute.cs deleted file mode 100644 index 91dce85..0000000 --- a/F5WafOrchestrator/JobAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator; - -[AttributeUsage(AttributeTargets.Class, Inherited = false)] -public class JobAttribute : Attribute -{ - public JobAttribute(string jobClass) - { - JobClassName = jobClass; - } - - private string JobClassName { get; } - - public virtual string JobClass => JobClassName; -} \ No newline at end of file diff --git a/F5WafOrchestrator/TLS/Discovery.cs b/F5WafOrchestrator/TLS/Discovery.cs index 51b38e6..f0306cf 100644 --- a/F5WafOrchestrator/TLS/Discovery.cs +++ b/F5WafOrchestrator/TLS/Discovery.cs @@ -1,3 +1,17 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + using Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.Client; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; @@ -6,47 +20,33 @@ namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.TLS; -[Job("Discovery")] -public class Discovery : Job, IDiscoveryJobExtension +public class Discovery : Job, IDiscoveryJobExtension { ILogger _logger = LogHandler.GetClassLogger(); - public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate cb) - { - _logger.LogDebug("Beginning F5 Distributed Cloud Discovery Job"); - - var result = new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId - }; + public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate cb) + { + _logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}..."); + _logger.LogDebug($"Server: {config.ClientMachine}"); - try - { - F5Client = new F5WafClient(config.ClientMachine, config.ServerPassword); - } catch (Exception ex) - { - _logger.LogError(ex, $"Could not connect to F5 Client" + ex.Message); - return result; - } - - List namespaces; - - try - { - namespaces = F5Client.DiscoverNamespacesforTlsStoreType(); - _logger.LogDebug($"Found {namespaces.Count()} namespaces in {config.ClientMachine}"); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error getting F5 namespaces from {config.ClientMachine}:\n" + ex.Message); - result.FailureMessage = $"Error getting F5 namespaces from {config.ClientMachine}:\n" + ex.Message; - return result; - } - - cb.Invoke(namespaces); + List namespaces; - result.Result = OrchestratorJobStatusJobResult.Success; - return result; + try + { + F5Client = new F5WafClient(config.ClientMachine, config.ServerPassword); + + namespaces = F5Client.DiscoverNamespacesforStoreType("tls-"); + _logger.LogDebug($"Found {namespaces.Count()} namespaces in {config.ClientMachine}"); + } + catch (Exception ex) + { + _logger.LogError($"Exception for {config.Capability}: {F5WAFException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = F5WAFException.FlattenExceptionMessages(ex, $"Server {config.ClientMachine}:") }; } + + cb.Invoke(namespaces); + + _logger.LogDebug($"...End {config.Capability} job for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; + } } \ No newline at end of file diff --git a/F5WafOrchestrator/TLS/Inventory.cs b/F5WafOrchestrator/TLS/Inventory.cs index c1c0eeb..d75c47c 100644 --- a/F5WafOrchestrator/TLS/Inventory.cs +++ b/F5WafOrchestrator/TLS/Inventory.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 Keyfactor // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,63 +17,62 @@ using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; +using System.Text.RegularExpressions; namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.TLS; -[Job("Inventory")] -public class Inventory : Job, IInventoryJobExtension +public class Inventory : Job, IInventoryJobExtension { private readonly ILogger _logger = LogHandler.GetClassLogger(); public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate cb) { - _logger.LogDebug("Beginning F5 Distributed Cloud Inventory Job"); - - var result = new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId - }; - - try - { - F5Client = new F5WafClient(config.CertificateStoreDetails.ClientMachine, config.ServerPassword); - } catch (Exception ex) - { - _logger.LogError(ex, $"Could not connect to F5 Client" + ex.Message); - return result; - } + _logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}..."); + _logger.LogDebug($"Server: {config.CertificateStoreDetails.ClientMachine}"); + _logger.LogDebug($"Store Path: {config.CertificateStoreDetails.StorePath}"); List inventoryItems; - string storePath = config.CertificateStoreDetails.StorePath; + try { - // check if the string starts with "ca-" and remove it if present + F5Client = new F5WafClient(config.CertificateStoreDetails.ClientMachine, config.ServerPassword); + + string storePath = config.CertificateStoreDetails.StorePath; + + // check if the string starts with "tls-" and remove it if present. This may occur + // if store was created in Command via a Discovery job if (config.CertificateStoreDetails.StorePath.StartsWith("tls-")) { - storePath = config.CertificateStoreDetails.StorePath.Substring(4); // Skip the first 3 characters ("ca-") + storePath = config.CertificateStoreDetails.StorePath.Substring(4); // Skip the first 4 characters ("tls-") } var (names, certs) = F5Client.TlsCertificateRetrievalProcess(storePath); inventoryItems = certs.Zip(names, (certificate, name) => new CurrentInventoryItem { Alias = name, - Certificates = new List { certificate }, - PrivateKeyEntry = false, - UseChainLevel = false + Certificates = Regex.Split(certificate, @"(?=-----BEGIN)").Where(p => p != string.Empty), + PrivateKeyEntry = true, + UseChainLevel = true }).ToList(); _logger.LogDebug($"Found {inventoryItems.Count} certificates in namespace {storePath}"); } catch (Exception ex) { - _logger.LogError(ex, $"Error getting F5 Certificate from namespace {storePath}:\n" + ex.Message); - result.FailureMessage = $"Error getting F5 Certificates from namespace {storePath}:\n" + ex.Message; - return result; + _logger.LogError($"Exception for {config.Capability}: {F5WAFException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = F5WAFException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}:") }; } - - cb.Invoke(inventoryItems); - result.Result = OrchestratorJobStatusJobResult.Success; - return result; + try + { + cb.Invoke(inventoryItems); + _logger.LogDebug($"...End {config.Capability} job for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; + } + catch (Exception ex) + { + string errorMessage = F5WAFException.FlattenExceptionMessages(ex, string.Empty); + _logger.LogError($"Exception returning certificates for {config.Capability}: {errorMessage} for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = F5WAFException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}:") }; + } } } \ No newline at end of file diff --git a/F5WafOrchestrator/TLS/Management.cs b/F5WafOrchestrator/TLS/Management.cs index c2af1f9..076df2b 100644 --- a/F5WafOrchestrator/TLS/Management.cs +++ b/F5WafOrchestrator/TLS/Management.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 Keyfactor // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,34 +20,22 @@ namespace Keyfactor.Extensions.Orchestrator.F5WafOrchestrator.TLS; -[Job("Management")] -public class Management : Job, IManagementJobExtension +public class Management : Job, IManagementJobExtension { ILogger _logger = LogHandler.GetClassLogger(); public JobResult ProcessJob(ManagementJobConfiguration config) { - _logger.LogDebug("Beginning F5 Cloud Management Job"); - - JobResult result = new JobResult - { - Result = OrchestratorJobStatusJobResult.Failure, - JobHistoryId = config.JobHistoryId - }; + _logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}..."); + _logger.LogDebug($"Server: {config.CertificateStoreDetails.ClientMachine}"); + _logger.LogDebug($"Store Path: {config.CertificateStoreDetails.StorePath}"); try { F5Client = new F5WafClient(config.CertificateStoreDetails.ClientMachine, config.ServerPassword); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Could not connect to F5 Client" + ex.Message); - return result; - } - try - { - // check if the string starts with "ca-" and remove it if present + // check if the string starts with "tls-" and remove it if present. This may occur + // if store was created in Command via a Discovery job if (config.CertificateStoreDetails.StorePath.StartsWith("tls-")) { config.CertificateStoreDetails.StorePath = config.CertificateStoreDetails.StorePath.Substring(4); @@ -56,40 +44,32 @@ public JobResult ProcessJob(ManagementJobConfiguration config) switch (config.OperationType) { case CertStoreOperationType.Add: - _logger.LogDebug("Adding certificate to F5 Cloud"); - - PerformTlsCertAddition(config, result); - - _logger.LogDebug("Add operation complete."); - - result.Result = OrchestratorJobStatusJobResult.Success; + _logger.LogDebug($"BEGIN add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}."); + PerformTlsCertAddition(config); + _logger.LogDebug($"END Add Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}."); break; case CertStoreOperationType.Remove: - _logger.LogDebug("Removing certificate from F5 Cloud"); - - PerformTlsCertRemove(config, result); - - _logger.LogDebug("Remove operation complete."); - - result.Result = OrchestratorJobStatusJobResult.Success; + _logger.LogDebug($"BEGIN Delete Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}."); + PerformTlsCertRemove(config); + _logger.LogDebug($"END Delete Operation for {config.CertificateStoreDetails.StorePath} on {config.CertificateStoreDetails.ClientMachine}."); break; default: - _logger.LogDebug("Invalid management operation type: {0}", config.OperationType); - throw new ArgumentOutOfRangeException(); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Unsupported operation: {config.OperationType.ToString()}" }; } } catch (Exception ex) { - _logger.LogError(ex, "Error processing job:\n {0}", ex.Message); - result.FailureMessage = ex.Message; - return result; + _logger.LogError($"Exception for {config.Capability}: {F5WAFException.FlattenExceptionMessages(ex, string.Empty)} for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = F5WAFException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}:") }; } - return result; + _logger.LogDebug($"...End {config.Capability} job for job id {config.JobId}"); + return new JobResult() { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; } - private void PerformTlsCertRemove(ManagementJobConfiguration config, JobResult result) + private void PerformTlsCertRemove(ManagementJobConfiguration config) { + _logger.MethodEntry(LogLevel.Debug); if (F5Client.JobCertIsAttachedToHttpLoadBalancer(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias)) @@ -98,44 +78,37 @@ private void PerformTlsCertRemove(ManagementJobConfiguration config, JobResult r "The job cert is bound to an http load balancer. Must unbind before performing management job."); } - F5Client.RemoveTlsCertificate(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias); + F5Client.RemoveCaOrTlsCertificate(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias, true); + + _logger.MethodExit(LogLevel.Debug); } - private void PerformTlsCertAddition(ManagementJobConfiguration config, JobResult result) + + private void PerformTlsCertAddition(ManagementJobConfiguration config) { - // ensure that the certificate is in PKCS#12 format - if (string.IsNullOrWhiteSpace(config.JobCertificate.PrivateKeyPassword)) - { - throw new Exception("Certificate must be in PKCS#12 format."); - } + _logger.MethodEntry(LogLevel.Debug); - // ensure that an alias is provided - if (string.IsNullOrWhiteSpace(config.JobCertificate.Alias)) - { - throw new Exception("Certificate alias is required."); - } + bool certificateExists = F5Client.CertificateExistsInF5(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias, true); - if (F5Client.CertificateExistsInF5(config.CertificateStoreDetails.StorePath, config.JobCertificate.Alias) && - !config.Overwrite) + if (certificateExists && !config.Overwrite) { string message = $"Certificate with alias \"{config.JobCertificate.Alias}\" already exists in F5, and job was not configured to overwrite."; throw new Exception(message); } - F5WafClient.PostRoot reqBody = F5Client.FormatTlsCertificateRequest(config.JobCertificate); - if (F5Client.CertificateExistsInF5(config.CertificateStoreDetails.StorePath, - config.JobCertificate.Alias) && - config.Overwrite) + if (certificateExists && config.Overwrite) { _logger.LogDebug("Overwrite is enabled, replacing certificate in F5 called \"{0}\"", config.JobCertificate.Alias); - F5Client.ReplaceTlsCertificate(config.CertificateStoreDetails.StorePath, reqBody); + F5Client.ReplaceCaOrTlsCertificate(config.CertificateStoreDetails.StorePath, reqBody, null, true); } else { _logger.LogDebug("Adding certificate to F5 Cloud"); - F5Client.AddTlsCertificate(config.CertificateStoreDetails.StorePath, reqBody); + F5Client.AddCaOrTlsCertificate(config.CertificateStoreDetails.StorePath, reqBody, null, true); } + + _logger.MethodExit(LogLevel.Debug); } } diff --git a/Images/image1.gif b/Images/image1.gif new file mode 100644 index 0000000..0f4576d Binary files /dev/null and b/Images/image1.gif differ diff --git a/Images/image2.gif b/Images/image2.gif new file mode 100644 index 0000000..af7d91d Binary files /dev/null and b/Images/image2.gif differ diff --git a/Images/image3.gif b/Images/image3.gif new file mode 100644 index 0000000..ac8fe56 Binary files /dev/null and b/Images/image3.gif differ diff --git a/Images/image4.gif b/Images/image4.gif new file mode 100644 index 0000000..ba3227d Binary files /dev/null and b/Images/image4.gif differ diff --git a/Images/image5.gif b/Images/image5.gif new file mode 100644 index 0000000..a69c772 Binary files /dev/null and b/Images/image5.gif differ diff --git a/Images/image6.gif b/Images/image6.gif new file mode 100644 index 0000000..2b8c047 Binary files /dev/null and b/Images/image6.gif differ diff --git a/README.md b/README.md index 6081edf..e737332 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -# f5-waf-orchestrator -The F5 WAF Orchestrator is an extension to the Keyfactor Universal Orchestrator. It Integrates with the Multi-Cloud App Connect, which is F5 Distributed Cloud's service for connecting apps across clouds, edge and on premises using load balancers. The purpose of the F5 WAF orchestrator is to manage the TLS certificates that are bound to the load balancers. This also includes managing the intermediate certificate chains and root CAs of these TLS certificates. The orchestrator facilitates the inventory, addition, removal, and discovery of certificates intended for use with load balancers. +# F5 WAF -#### Integration status: Prototype +The F5 WAF Orchestrator is an extension to the Keyfactor Universal Orchestrator. It Integrates with Multi-Cloud App Connect, which is F5 Distributed Cloud's service for connecting apps across clouds and within on premise installationss using load balancers. The purpose of the F5 WAF orchestrator is to manage the TLS and CA Root certificates. The TLS certificates may be bound to load balancers. The orchestrator facilitates the inventory, addition, renewal, and removal of these certificates as well as the discovery of namespaces (certificate stores) within the F5 Multi-Cloud App Connect instance. + +#### Integration status: Production - Ready for use in production environments. ## About the Keyfactor Universal Orchestrator Extension @@ -12,9 +13,9 @@ The Universal Orchestrator is part of the Keyfactor software distribution and is The Universal Orchestrator is the successor to the Windows Orchestrator. This Orchestrator Extension plugin only works with the Universal Orchestrator and does not work with the Windows Orchestrator. -## Support for F5 WAF Orchestrator +## Support for F5 WAF -The F5 WAF Orchestrator is open source and supported on best effort level for this tool/library/client. This means customers can report Bugs, Feature Requests, Documentation amendment or questions as well as requests for customer information required for setup that needs Keyfactor access to obtain. Such requests do not follow normal SLA commitments for response or resolution. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com/. +F5 WAF is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com ###### 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. @@ -23,10 +24,11 @@ The F5 WAF Orchestrator is open source and supported on best effort level for th --- -## Keyfactor Version Supported -The minimum version of the Keyfactor Universal Orchestrator Framework needed to run this version of the extension is 10.2. +## Keyfactor Version Supported + +The minimum version of the Keyfactor Universal Orchestrator Framework needed to run this version of the extension is 10.4.1 ## Platform Specific Notes 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. @@ -36,162 +38,240 @@ The Keyfactor Universal Orchestrator may be installed on either Windows or Linux |Supports Management Remove|✓ |✓ | |Supports Create Store| | | |Supports Discovery|✓ |✓ | -|Supports Renrollment| | | +|Supports Reenrollment| | | |Supports Inventory|✓ |✓ | ---- -## Overview -The F5 WAF Orchestrator extension remotely manages certificates uploaded to F5 Distributed Cloud Multi-App Connect, which is the F5 platform that manages WAF services. Once in Multi-App Connect, certificates can be associated to configured HTTP load balancers. +## PAM Integration + +This orchestrator extension has the ability to connect to a variety of supported PAM providers to allow for the retrieval of various client hosted secrets right from the orchestrator server itself. This eliminates the need to set up the PAM integration on Keyfactor Command which may be in an environment that the client does not want to have access to their PAM provider. + +The secrets that this orchestrator extension supports for use with a PAM Provider are: + +|Name|Description| +|----|-----------| +|ServerUsername|The user id that will be used to authenticate to the F5 WAF API endpoints| +|ServerPassword|The API token that will be used to authenticate to the F5 WAF API endpoints| + + +It is not necessary to use a PAM Provider for all of the secrets available above. If a PAM Provider should not be used, simply enter in the actual value to be used, as normal. + +If a PAM Provider will be used for one of the fields above, start by referencing the [Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam). The GitHub repo for the PAM Provider to be used contains important information such as the format of the `json` needed. What follows is an example but does not reflect the `json` values for all PAM Providers as they have different "instance" and "initialization" parameter names and values. + +
General PAM Provider Configuration +

+ + + +### Example PAM Provider Setup + +To use a PAM Provider to resolve a field, in this example the __Server Password__ will be resolved by the `Hashicorp-Vault` provider, first install the PAM Provider extension from the [Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam) on the Universal Orchestrator. + +Next, complete configuration of the PAM Provider on the UO by editing the `manifest.json` of the __PAM Provider__ (e.g. located at extensions/Hashicorp-Vault/manifest.json). The "initialization" parameters need to be entered here: + +~~~ json + "Keyfactor:PAMProviders:Hashicorp-Vault:InitializationInfo": { + "Host": "http://127.0.0.1:8200", + "Path": "v1/secret/data", + "Token": "xxxxxx" + } +~~~ + +After these values are entered, the Orchestrator needs to be restarted to pick up the configuration. Now the PAM Provider can be used on other Orchestrator Extensions. + +### Use the PAM Provider +With the PAM Provider configured as an extenion on the UO, a `json` object can be passed instead of an actual value to resolve the field with a PAM Provider. Consult the [Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam) for the specific format of the `json` object. + +To have the __Server Password__ field resolved by the `Hashicorp-Vault` provider, the corresponding `json` object from the `Hashicorp-Vault` extension needs to be copied and filed in with the correct information: + +~~~ json +{"Secret":"my-kv-secret","Key":"myServerPassword"} +~~~ -## Use Cases +This text would be entered in as the value for the __Server Password__, instead of entering in the actual password. The Orchestrator will attempt to use the PAM Provider to retrieve the __Server Password__. If PAM should not be used, just directly enter in the value for the field. +

+
-The F5 Orchestrator supports two different types of certificates stores with the capabilities for each below: -- Root CAs (f5WafCa) - - Discovery - - Inventory - - Management (Add and Remove) -- TLS Certificates (f5WafTls) - - Discovery - - Inventory - - Management (Add and Remove) -## F5 WAF Orchestrator Installation -Assuming the Keyfactor Universal Orchestrator Service is already installed... +--- + -1. Stop the Keyfactor Universal Orchestrator Service. -3. Clone the F5 WAF Orchestrator from GitHub on your local machine. -4. Navigate to the F5 WAF Orchestrator home directory and build the solution. Upon building the solution, the F5 WAF Orchestrator extension will automatically be added to the local Keyfactor Universal Orchestrator. -5. Start the Keyfactor Universal Orchestrator Service. +## Overview +The F5 WAF Orchestrator extension remotely manages TLS and CA Root certificates uploaded to F5 Distributed Multi-Cloud App Connect, which is the F5 platform that manages WAF services. Certificates bound to Http Load Balancers within Multi-Cloud App Connect can be renewed/replaced, but they cannot be removed. Certificate store types f5WafTls and f5WafCa are used to manage stores containing TLS and CA Root certificates, respectively. -## F5 WAF Orchestrator Configuration +
+f5WafTls -Below are the steps for manually configuring the F5 WAF Orchestrator in Keyfactor Command, assuming the Keyfactor Universal Orchestrator is installed and has the F5 WAF Orchestrator extension: +The f5WafTls certificate store type is used to manage F5 Distributed Multi-Cloud App Connect TLS certificates. -1. Create F5WafTls and F5WafCa Certificate Store Types for the F5 WAF Orchestrator extension. -2. Create certificate stores in Keyfactor Command for the F5WafTls and F5WafCa certificate store types. +Use cases supported: +1. Discovery of TLS stores. Discovery for F5 WAF returns any discoverable namespaces in the F5 WAF instance. +2. Inventory of a TLS store. All TLS certificates, bound or unbound, within a namespace will be returned to Keyfactor Command. +3. Management-Add. Add a new certificate or renew an existing one. Renew will work for both bound and unbound certificates. All existing binding will remain in place, bound to the same alias with the newly replaced/renewed certificate. +4. Management-Delete. Remove an existing certificate. Will only work for unbound certificates. -View the Certificate Store Type and certificate store configuration instructions for the F5 WAF Orchestrator below. +
-## F5WafTls Certificate Store Type Configuration +
+f5WafCa -The `F5WafTls` Certificate Store Type can be created manually by following the below steps: +The f5WafCa certificate store type is used to manage F5 Distributed Multi-Cloud App Connect CA Root certificates. -Create a store type called `F5WafTls` with the attributes in the tables below: +Use cases supported: +1. Discovery of TLS stores. Discovery for F5 WAF returns any discoverable namespaces in the F5 WAF instance. +2. Inventory of a TLS store. All CA Root certificates within a namespace will be returned to Keyfactor Command. +3. Management-Add. Add a new certificate or renew an existing one. +4. Management-Delete. Remove an existing certificate. Please note, for CA Root certicates, deleting an existing certificate will replace ALL instances of the same certificate and not only the one represented by the intended alias. This is an F5 WAF feature that the integration has no control over. -### Basic Tab -| Attribute | Value | Description | -| --------- |----------| ----- | -| Name | F5WafTls | Display name for the store type (may be customized) | -| Short Name | F5WafTls | Short display name for the store type | -| Capability | | Store type name orchestrator will register with. Check the box to allow entry of value | -| Supported Job Types (check the box for each) | Inventory, Add, Discovery, Remove | Job types the extension supports | -| Needs Server | ✓ | Determines if a target server name is required when creating store | -| Blueprint Allowed | | Determines if store type may be included in an Orchestrator blueprint | -| Uses PowerShell | | Determines if underlying implementation is PowerShell | -| Requires Store Password | | Determines if a store password is required when configuring an individual store. | -| Supports Entry Password | | Determines if an individual entry within a store can have a password. | +
-The Basic tab should look like this: +## F5 WAF Orchestrator Extension Installation -![Insert Image](../.github/images/AzureApp-basic-store-type-dialog.png) +1. Refer to the [Creating Certificate Store Types](#creating-certificate-store-types) section to create the certificate store types you wish to manage. +2. Stop the Keyfactor Universal Orchestrator Service on the server you plan to install this extension to run on. +3. In the Keyfactor Orchestrator installation folder (by convention usually C:\Program Files\Keyfactor\Keyfactor Orchestrator for a Windows install or /opt/keyfactor/orchestrator/ for a Linux install), find the "Extensions" folder. Underneath that, create a new folder named "F5Waf". You may choose to use a different name if you wish. +4. Download the latest version of the F5 WAF orchestrator extension from [GitHub](https://github.com/Keyfactor/f5-waf-orchestrator). Click on the "Latest" release link on the right hand side of the main page and download the first zip file. +5. Copy the contents of the download installation zip file to the folder created in step 3. +6. (Optional) If you decide to create one or more certificate store types with short names different than the suggested values, edit the manifest.json file in the folder you created in step 3, and modify each "ShortName" in each "Certstores.{ShortName}.{Operation}" line with the ShortName you used to create the respective certificate store type. +7. Start the Keyfactor Universal Orchestrator Service. -### 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. | -| 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.) | +## Creating Certificate Store Types -The Advanced tab should look like this: +Below are the two certificate store types that the F5 WAF Orchestator Extension manages. To create a new Certificate Store Type in Keyfactor Command, first click on settings (the gear icon on the top right) => Certificate Store Types => Add. Next, follow the instructions under each store type you wish to set up. -![Insert Image](../.github/images/AzureApp-advanced-store-type-dialog.png) +
+f5WafTls - TLS certificates in a namespace -### 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: +- Basic Tab: -| Name | Display Name | Type | Default Value/Options | Required | Description | -| ---- | ------------ | ---- | --------------------- | -------- |-------------------| -| ServerUsername | Server Username | Secret | | ✓ | The username used to log in to the F5 Distributed Cloud instance (typically an email). | -| ServerPassword | Server Password | Secret | | ✓ | The API Token configured in the F5 Distributed Cloud instance's Account Settings. | -| ServerUseSsl | Use SSL | Bool | true | ✓ | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. | + - **Name** – Required. The display name you wish to use for the new Certificate Store Type. + - **Short Name** – Required. Suggested value - **f5WafTls**. If you choose to use a different value you must make the corresponding modification to the manifest.json file. See [F5 WAF Orchestrator Extension Installation](#f5-waf-orchestrator-extension-installation), step 6 above. + - **Custom Capability** - Unchecked + - **Supported Job Types** - Inventory, Add, Remove, and Discovery should all be checked. + - **Needs Server** - Checked + - **Blueprint Allowed** - Checked if you wish to make use of blueprinting. Please refer to the Keyfactor Command Reference Guide for more details on this feature. + - **Uses PowerShell** - Unchecked + - **Requires Store Password** - Unchecked + - **Supports Entry Password** - Unchecked +- Advanced Tab: -The Custom Fields tab should look like this: + - **Store Path Type** - Freeform + - **Supports Custom Alias** - Required. + - **Private Key Handling** - Required. + - **PFX Password Style** - Default -![Insert Image](../.github/images/AzureApp-custom-fields-store-type-dialog.png) +- Custom Fields Tab: -## F5WafCa Certificate Store Type Configuration + - no additional custom fields -The `F5WafCa` Certificate Store Type can be created manually by following the below steps: +- Entry Parameters Tab: -Create a store type called `F5WafCa` with the attributes in the tables below: + - no additional entry parameters -### Basic Tab -| Attribute | Value | Description | -| --------- |----------| ----- | -| Name | F5WafTls | Display name for the store type (may be customized) | -| Short Name | F5WafTls | Short display name for the store type | -| Capability | | Store type name orchestrator will register with. Check the box to allow entry of value | -| Supported Job Types (check the box for each) | Inventory, Add, Discovery, Remove | Job types the extension supports | -| Needs Server | ✓ | Determines if a target server name is required when creating store | -| Blueprint Allowed | | Determines if store type may be included in an Orchestrator blueprint | -| Uses PowerShell | | Determines if underlying implementation is PowerShell | -| Requires Store Password | | Determines if a store password is required when configuring an individual store. | -| Supports Entry Password | | Determines if an individual entry within a store can have a password. | +
+
+f5WafCa - CA Root certificates in a namespace -The Basic tab should look like this: +- Basic Tab: -![Insert Image](../.github/images/AzureApp-basic-store-type-dialog.png) + - **Name** – Required. The display name you wish to use for the new Certificate Store Type. + - **Short Name** – Required. Suggested value - **f5WafCa**. If you choose to use a different value you must make the corresponding modification to the manifest.json file. See [F5 WAF Orchestrator Extension Installation](#f5-waf-orchestrator-extension-installation), step 6 above. + - **Custom Capability** - Unchecked + - **Supported Job Types** - Inventory, Add, Remove, and Discovery should all be checked. + - **Needs Server** - Checked + - **Blueprint Allowed** - Checked if you wish to make use of blueprinting. Please refer to the Keyfactor Command Reference Guide for more details on this feature. + - **Uses PowerShell** - Unchecked + - **Requires Store Password** - Unchecked + - **Supports Entry Password** - Unchecked -### 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 | Forbidden | This determines if Keyfactor can send the private key associated with a certificate to the store. | -| PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) | +- Advanced Tab: + - **Store Path Type** - Freeform + - **Supports Custom Alias** - Required. + - **Private Key Handling** - Forbidden. + - **PFX Password Style** - Default -The Advanced tab should look like this: +- Custom Fields Tab: -![Insert Image](../.github/images/AzureApp-advanced-store-type-dialog.png) + - no additional custom fields -### 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: +- Entry Parameters Tab: -| Name | Display Name | Type | Default Value/Options | Required | Description | -| ---- | ------------ | ---- | --------------------- | -------- |-------------------| -| ServerUsername | Server Username | Secret | | ✓ | The username used to log in to the F5 Distributed Cloud instance (typically an email). | -| ServerPassword | Server Password | Secret | | ✓ | The API Token configured in the F5 Distributed Cloud instance's Account Settings. | -| ServerUseSsl | Use SSL | Bool | true | ✓ | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. | + - no additional entry parameters +
-The Custom Fields tab should look like this: -![Insert Image](../.github/images/AzureApp-custom-fields-store-type-dialog.png) +## Creating Certificate Stores and Scheduling Discovery Jobs -## F5WafTls and F5WafCa Certificate Store Configurations +When creating new certificate stores or scheduling discovery jobs in Keyfactor Command, there are a few fields that are important to highlight here: -After creating the `F5WafTls` and `F5WafCa` Certificate Store Types and installing the F5 WAF Orchestrator extension, you can create new [Certificate Stores](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store) to manage certificates in the remote platform. +
+Certificate Stores -The following table describes the required and optional fields for the `F5WafTls` and `F5WafCa` certificate store types. +The following table describes the required and optional fields for the `f5WafTls` and `f5WafCa` certificate store types when creating a certificate store. -In Keyfactor Command, navigate to Certificate Stores from the Locations Menu. Click the Add button to create a new Certificate Store using the attributes in the table above. +In Keyfactor Command, navigate to Certificate Stores from the Locations Menu. Click the Add button to create a new Certificate Store. | Attribute | Description | | --------- |------------------------------------------------------------------------------------------------------------------------------------------------| -| Category | Select either F5WafTls or F5WafCa depending on whether you want to manage TLS certificates or Root CAs. | +| Category | Select either f5WafTls or f5WafCa depending on whether you want to manage TLS certificates or Root CA certificates. | | Container | Optional container to associate certificate store with. | -| Client Machine | The URL for the F5 Distributed Cloud instance (typically ending in '.console.ves.volterra.io'. | +| Client Machine | The URL for the F5 Distributed Cloud instance (typically ending in '.console.ves.volterra.io'). | | Store Path | The Multi-Cloud App Connect namespace containing the certificates you wish to manage. | -| Orchestrator | Select an approved orchestrator capable of managing F5 WAF certificates. Specifically, one with the F5WafCa and F5WafTls capabilities. | +| Orchestrator | Select an approved orchestrator capable of managing F5 WAF certificates. Specifically, one with the f5WafTls and f5WafCa capabilities. | +| Server Username | The username used to log in to the F5 Distributed Cloud instance (typically an email). | +| Server Password | The API Token configured in the F5 Distributed Cloud instance's Account Settings. Please see [Creating an F5 WAF API Token](#creating-an-f5-waf-api-token) for more details on creating this token. | +| Use SSL | Not used for this integration, so either setting is fine. | + +
+ +
+Discovery Jobs + +The following table describes the required and optional fields to schedule a Discovery job for the `f5WafTls` and `f5WafCa` certificate store types. + +In Keyfactor Command, navigate to Certificate Stores from the Locations Menu and then click on the Discover tab. + +| Attribute | Description | +| --------- |------------------------------------------------------------------------------------------------------------------------------------------------| +| Category | Select either F5WafTls or F5WafCa depending on whether you want to return namespaces for TLS certificates or CA Root certificates. | +| Orchestrator | Select an approved orchestrator capable of managing F5 WAF certificates. Specifically, one with the f5WafTls and f5WafCa capabilities. | +| Schedule | Enter the schedule for when you want the job to run | +| Client Machine | The URL for the F5 Distributed Cloud instance (typically ending in '.console.ves.volterra.io'. | | Server Username | The username used to log in to the F5 Distributed Cloud instance (typically an email). | -| Server Password | The API Token configured in the F5 Distributed Cloud instance's Account Settings. | -| Use SSL | Specifies whether SSL should be used for communication with the server. Set to 'true' to enable SSL, and 'false' to disable it. | +| Server Password | The API Token configured in the F5 Distributed Cloud instance's Account Settings. Please see [Creating an F5 WAF API Token](#creating-an-f5-waf-api-token) for more details on creating this token. | +| Directories to Search | Not used for this integration. Leave Blank. | +| Directories to ignore | Not used for this integration. Leave Blank. | +| Extensions | Not used for this integration. Leave Blank. | +| File name patterns to match | Not used for this integration. Leave Blank. | +| Follow SymLinks | Not used for this integration. Leave Unchecked. | +| Follow SymLinks | Not used for this integration. Leave Unchecked. | +| Use SSL? | Not used for this integration. Leave Unchecked. | + +Discovery jobs will return all known namespaces for this F5 WAF instance. Please note that because Keyfactor Command has a restriction on multiple certificate stores having the same Client Machine and Store Path, certificate stores for f5WafTls will return stores with a "tls-" prefixed to the beginning of the store path (namespace); while f5WafCA stores will have "ca-" prefixed. Any jobs that run for stores with these prefixes will have these prefixes removed before calling any F5 WAF APIs. What this means is a store path (namespace) for an f5WafTls store of "tls-namespace1" will be the same as one labeled "namespace1". + +
+ + +## Creating an F5 WAF API Token + +In lieu of providing a server password when setting up an F5 WAF certificate store, F5 Multi-Cloud App Connect uses API tokens combined with the user id to authenticate when calling APIs. API Tokens can be created through the F5 Distributed Cloud Console after logging in with the ID you wish to use for the Keyfactor certificate store. Once logged in, select Multi-Cloud App Connect from the options under "Common services". Next, select Account Services from the pull down at the top right of the screen, and select "Account Settings". From there, click on "Credentials" on the left nav and "Add Credentials" on the subsequent screen. In the form shown, select "API Token" from the Credential Type dropdown, and enter the name of the credential and the expiration date. Please note that credentials can only be created for up to 90 day periods of time. After 90 days, a new API token will need to be generated and replaced in your F5 WAF certificate store(s). Clicking Generate will then show the value of the newly created API Token. Copy this and save to a safe place, as this will be the value you will enter in the Server Password field when setting up your certificate store. If you forget or lose this token value, there is no way to access it again in the F5 Distributed Cloud portal. You will need to create a new API Token. + +![](Images/image1.gif) +![](Images/image2.gif) +![](Images/image3.gif) +![](Images/image4.gif) +![](Images/image5.gif) +![](Images/image6.gif) + + +When creating cert store type manually, that store property names and entry parameter names are case sensitive + --- diff --git a/integration-manifest.json b/integration-manifest.json new file mode 100644 index 0000000..7cd4ebc --- /dev/null +++ b/integration-manifest.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", + "integration_type": "orchestrator", + "name": "F5 WAF", + "status": "production", + "link_github": true, + "update_catalog": true, + "release_dir": "F5WafOrchestrator/bin/Release", + "support_level": "kf-supported", + "description": "The F5 WAF Orchestrator is an extension to the Keyfactor Universal Orchestrator. It Integrates with Multi-Cloud App Connect, which is F5 Distributed Cloud's service for connecting apps across clouds and within on premise installationss using load balancers. The purpose of the F5 WAF orchestrator is to manage the TLS and CA Root certificates. The TLS certificates may be bound to load balancers. The orchestrator facilitates the inventory, addition, renewal, and removal of these certificates as well as the discovery of namespaces (certificate stores) within the F5 Multi-Cloud App Connect instance.", + "about": { + "orchestrator": { + "UOFramework": "10.4.1", + "pam_support": true, + "keyfactor_platform_version": "9.10", + "win": { + "supportsCreateStore": false, + "supportsDiscovery": true, + "supportsManagementAdd": true, + "supportsManagementRemove": true, + "supportsReenrollment": false, + "supportsInventory": true, + "platformSupport": "Unused" + }, + "linux": { + "supportsCreateStore": false, + "supportsDiscovery": true, + "supportsManagementAdd": true, + "supportsManagementRemove": true, + "supportsReenrollment": false, + "supportsInventory": true, + "platformSupport": "Unused" + }, + "store_types": { + "f5WafTls": { + "Name": "F5 WAF TLS", + "ShortName": "f5WafTls", + "Capability": "f5WafTls", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "Properties": [], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "PrivateKeyAllowed": "Required", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": true, + "CustomAliasAllowed": "Required" + }, + "f5WafCa": { + "Name": "F5 WAF CA", + "ShortName": "f5WafCa", + "Capability": "f5WafCa", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": true, + "Enrollment": false, + "Remove": true + }, + "Properties": [], + "EntryParameters": [], + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "PrivateKeyAllowed": "Forbidden", + "JobProperties": [], + "ServerRequired": true, + "PowerShell": false, + "BlueprintAllowed": true, + "CustomAliasAllowed": "Required" + } + } + } + } +} diff --git a/readme-src/readme-pam-support.md b/readme-src/readme-pam-support.md new file mode 100644 index 0000000..afa937b --- /dev/null +++ b/readme-src/readme-pam-support.md @@ -0,0 +1,5 @@ +|Name|Description| +|----|-----------| +|ServerUsername|The user id that will be used to authenticate to the F5 WAF API endpoints| +|ServerPassword|The API token that will be used to authenticate to the F5 WAF API endpoints| + diff --git a/readme_source.md b/readme_source.md new file mode 100644 index 0000000..bacb491 --- /dev/null +++ b/readme_source.md @@ -0,0 +1,173 @@ +## Overview +The F5 WAF Orchestrator extension remotely manages TLS and CA Root certificates uploaded to F5 Distributed Multi-Cloud App Connect, which is the F5 platform that manages WAF services. Certificates bound to Http Load Balancers within Multi-Cloud App Connect can be renewed/replaced, but they cannot be removed. Certificate store types f5WafTls and f5WafCa are used to manage stores containing TLS and CA Root certificates, respectively. + +
+f5WafTls + +The f5WafTls certificate store type is used to manage F5 Distributed Multi-Cloud App Connect TLS certificates. + +Use cases supported: +1. Discovery of TLS stores. Discovery for F5 WAF returns any discoverable namespaces in the F5 WAF instance. +2. Inventory of a TLS store. All TLS certificates, bound or unbound, within a namespace will be returned to Keyfactor Command. +3. Management-Add. Add a new certificate or renew an existing one. Renew will work for both bound and unbound certificates. All existing binding will remain in place, bound to the same alias with the newly replaced/renewed certificate. +4. Management-Delete. Remove an existing certificate. Will only work for unbound certificates. + +
+ +
+f5WafCa + +The f5WafCa certificate store type is used to manage F5 Distributed Multi-Cloud App Connect CA Root certificates. + +Use cases supported: +1. Discovery of TLS stores. Discovery for F5 WAF returns any discoverable namespaces in the F5 WAF instance. +2. Inventory of a TLS store. All CA Root certificates within a namespace will be returned to Keyfactor Command. +3. Management-Add. Add a new certificate or renew an existing one. +4. Management-Delete. Remove an existing certificate. Please note, for CA Root certicates, deleting an existing certificate will replace ALL instances of the same certificate and not only the one represented by the intended alias. This is an F5 WAF feature that the integration has no control over. + +
+ + +## F5 WAF Orchestrator Extension Installation + +1. Refer to the [Creating Certificate Store Types](#creating-certificate-store-types) section to create the certificate store types you wish to manage. +2. Stop the Keyfactor Universal Orchestrator Service on the server you plan to install this extension to run on. +3. In the Keyfactor Orchestrator installation folder (by convention usually C:\Program Files\Keyfactor\Keyfactor Orchestrator for a Windows install or /opt/keyfactor/orchestrator/ for a Linux install), find the "Extensions" folder. Underneath that, create a new folder named "F5Waf". You may choose to use a different name if you wish. +4. Download the latest version of the F5 WAF orchestrator extension from [GitHub](https://github.com/Keyfactor/f5-waf-orchestrator). Click on the "Latest" release link on the right hand side of the main page and download the first zip file. +5. Copy the contents of the download installation zip file to the folder created in step 3. +6. (Optional) If you decide to create one or more certificate store types with short names different than the suggested values, edit the manifest.json file in the folder you created in step 3, and modify each "ShortName" in each "Certstores.{ShortName}.{Operation}" line with the ShortName you used to create the respective certificate store type. +7. Start the Keyfactor Universal Orchestrator Service. + + +## Creating Certificate Store Types + +Below are the two certificate store types that the F5 WAF Orchestator Extension manages. To create a new Certificate Store Type in Keyfactor Command, first click on settings (the gear icon on the top right) => Certificate Store Types => Add. Next, follow the instructions under each store type you wish to set up. + +
+f5WafTls - TLS certificates in a namespace + +- Basic Tab: + + - **Name** – Required. The display name you wish to use for the new Certificate Store Type. + - **Short Name** – Required. Suggested value - **f5WafTls**. If you choose to use a different value you must make the corresponding modification to the manifest.json file. See [F5 WAF Orchestrator Extension Installation](#f5-waf-orchestrator-extension-installation), step 6 above. + - **Custom Capability** - Unchecked + - **Supported Job Types** - Inventory, Add, Remove, and Discovery should all be checked. + - **Needs Server** - Checked + - **Blueprint Allowed** - Checked if you wish to make use of blueprinting. Please refer to the Keyfactor Command Reference Guide for more details on this feature. + - **Uses PowerShell** - Unchecked + - **Requires Store Password** - Unchecked + - **Supports Entry Password** - Unchecked + +- Advanced Tab: + + - **Store Path Type** - Freeform + - **Supports Custom Alias** - Required. + - **Private Key Handling** - Required. + - **PFX Password Style** - Default + +- Custom Fields Tab: + + - no additional custom fields + +- Entry Parameters Tab: + + - no additional entry parameters + +
+ +
+f5WafCa - CA Root certificates in a namespace + +- Basic Tab: + + - **Name** – Required. The display name you wish to use for the new Certificate Store Type. + - **Short Name** – Required. Suggested value - **f5WafCa**. If you choose to use a different value you must make the corresponding modification to the manifest.json file. See [F5 WAF Orchestrator Extension Installation](#f5-waf-orchestrator-extension-installation), step 6 above. + - **Custom Capability** - Unchecked + - **Supported Job Types** - Inventory, Add, Remove, and Discovery should all be checked. + - **Needs Server** - Checked + - **Blueprint Allowed** - Checked if you wish to make use of blueprinting. Please refer to the Keyfactor Command Reference Guide for more details on this feature. + - **Uses PowerShell** - Unchecked + - **Requires Store Password** - Unchecked + - **Supports Entry Password** - Unchecked + +- Advanced Tab: + + - **Store Path Type** - Freeform + - **Supports Custom Alias** - Required. + - **Private Key Handling** - Forbidden. + - **PFX Password Style** - Default + +- Custom Fields Tab: + + - no additional custom fields + +- Entry Parameters Tab: + + - no additional entry parameters + +
+ + +## Creating Certificate Stores and Scheduling Discovery Jobs + +When creating new certificate stores or scheduling discovery jobs in Keyfactor Command, there are a few fields that are important to highlight here: + +
+Certificate Stores + +The following table describes the required and optional fields for the `f5WafTls` and `f5WafCa` certificate store types when creating a certificate store. + +In Keyfactor Command, navigate to Certificate Stores from the Locations Menu. Click the Add button to create a new Certificate Store. + +| Attribute | Description | +| --------- |------------------------------------------------------------------------------------------------------------------------------------------------| +| Category | Select either f5WafTls or f5WafCa depending on whether you want to manage TLS certificates or Root CA certificates. | +| Container | Optional container to associate certificate store with. | +| Client Machine | The URL for the F5 Distributed Cloud instance (typically ending in '.console.ves.volterra.io'). | +| Store Path | The Multi-Cloud App Connect namespace containing the certificates you wish to manage. | +| Orchestrator | Select an approved orchestrator capable of managing F5 WAF certificates. Specifically, one with the f5WafTls and f5WafCa capabilities. | +| Server Username | The username used to log in to the F5 Distributed Cloud instance (typically an email). | +| Server Password | The API Token configured in the F5 Distributed Cloud instance's Account Settings. Please see [Creating an F5 WAF API Token](#creating-an-f5-waf-api-token) for more details on creating this token. | +| Use SSL | Not used for this integration, so either setting is fine. | + +
+ +
+Discovery Jobs + +The following table describes the required and optional fields to schedule a Discovery job for the `f5WafTls` and `f5WafCa` certificate store types. + +In Keyfactor Command, navigate to Certificate Stores from the Locations Menu and then click on the Discover tab. + +| Attribute | Description | +| --------- |------------------------------------------------------------------------------------------------------------------------------------------------| +| Category | Select either F5WafTls or F5WafCa depending on whether you want to return namespaces for TLS certificates or CA Root certificates. | +| Orchestrator | Select an approved orchestrator capable of managing F5 WAF certificates. Specifically, one with the f5WafTls and f5WafCa capabilities. | +| Schedule | Enter the schedule for when you want the job to run | +| Client Machine | The URL for the F5 Distributed Cloud instance (typically ending in '.console.ves.volterra.io'. | +| Server Username | The username used to log in to the F5 Distributed Cloud instance (typically an email). | +| Server Password | The API Token configured in the F5 Distributed Cloud instance's Account Settings. Please see [Creating an F5 WAF API Token](#creating-an-f5-waf-api-token) for more details on creating this token. | +| Directories to Search | Not used for this integration. Leave Blank. | +| Directories to ignore | Not used for this integration. Leave Blank. | +| Extensions | Not used for this integration. Leave Blank. | +| File name patterns to match | Not used for this integration. Leave Blank. | +| Follow SymLinks | Not used for this integration. Leave Unchecked. | +| Follow SymLinks | Not used for this integration. Leave Unchecked. | +| Use SSL? | Not used for this integration. Leave Unchecked. | + +Discovery jobs will return all known namespaces for this F5 WAF instance. Please note that because Keyfactor Command has a restriction on multiple certificate stores having the same Client Machine and Store Path, certificate stores for f5WafTls will return stores with a "tls-" prefixed to the beginning of the store path (namespace); while f5WafCA stores will have "ca-" prefixed. Any jobs that run for stores with these prefixes will have these prefixes removed before calling any F5 WAF APIs. What this means is a store path (namespace) for an f5WafTls store of "tls-namespace1" will be the same as one labeled "namespace1". + +
+ + +## Creating an F5 WAF API Token + +In lieu of providing a server password when setting up an F5 WAF certificate store, F5 Multi-Cloud App Connect uses API tokens combined with the user id to authenticate when calling APIs. API Tokens can be created through the F5 Distributed Cloud Console after logging in with the ID you wish to use for the Keyfactor certificate store. Once logged in, select Multi-Cloud App Connect from the options under "Common services". Next, select Account Services from the pull down at the top right of the screen, and select "Account Settings". From there, click on "Credentials" on the left nav and "Add Credentials" on the subsequent screen. In the form shown, select "API Token" from the Credential Type dropdown, and enter the name of the credential and the expiration date. Please note that credentials can only be created for up to 90 day periods of time. After 90 days, a new API token will need to be generated and replaced in your F5 WAF certificate store(s). Clicking Generate will then show the value of the newly created API Token. Copy this and save to a safe place, as this will be the value you will enter in the Server Password field when setting up your certificate store. If you forget or lose this token value, there is no way to access it again in the F5 Distributed Cloud portal. You will need to create a new API Token. + +![](Images/image1.gif) +![](Images/image2.gif) +![](Images/image3.gif) +![](Images/image4.gif) +![](Images/image5.gif) +![](Images/image6.gif) +