venafi-connector-machine 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle.mjs +392 -112
- package/package.json +1 -1
package/bundle.mjs
CHANGED
|
@@ -29951,11 +29951,11 @@ var StdioServerTransport = class {
|
|
|
29951
29951
|
var MACHINE_BLUEPRINT = `
|
|
29952
29952
|
# Venafi Machine Connector Blueprint
|
|
29953
29953
|
|
|
29954
|
-
This document captures the machine connector-specific architecture, patterns, and structure for building a Venafi TLS Protect Cloud machine connector. It is distilled from building multiple connectors
|
|
29954
|
+
This document captures the machine connector-specific architecture, patterns, and structure for building a Venafi TLS Protect Cloud machine connector. It is distilled from building multiple connectors across SSH-based targets, REST API appliances, and API management platforms, as well as the Venafi Machine Connector Framework documentation.
|
|
29955
29955
|
|
|
29956
29956
|
## What Is a Machine Connector?
|
|
29957
29957
|
|
|
29958
|
-
A machine connector is a containerized Go REST service that runs on a Venafi vSatellite. It acts as middleware between Venafi TLS Protect Cloud and a target system (e.g.,
|
|
29958
|
+
A machine connector is a containerized Go REST service that runs on a Venafi vSatellite. It acts as middleware between Venafi TLS Protect Cloud and a target system (e.g., web servers, app servers, load balancers, network appliances). The connector discovers certificates on the target, reports them to Venafi, and provisions/renews certificates when instructed.
|
|
29959
29959
|
|
|
29960
29960
|
- **pluginType**: "MACHINE"
|
|
29961
29961
|
- **workTypes**: ["PROVISIONING", "DISCOVERY"]
|
|
@@ -30213,7 +30213,7 @@ Keystore and binding fields can be turned into dropdowns populated from the targ
|
|
|
30213
30213
|
2. Implementing \`getTargetConfiguration\` to return actual values (not a stub)
|
|
30214
30214
|
3. Adding \`x-targetConfigurationRef\` to the field in keystore/binding
|
|
30215
30215
|
|
|
30216
|
-
**Example**:
|
|
30216
|
+
**Example**: Virtual domains (partitions) as a dropdown:
|
|
30217
30217
|
|
|
30218
30218
|
\`\`\`json
|
|
30219
30219
|
"targetConfiguration": {
|
|
@@ -30236,7 +30236,7 @@ Keystore and binding fields can be turned into dropdowns populated from the targ
|
|
|
30236
30236
|
}
|
|
30237
30237
|
\`\`\`
|
|
30238
30238
|
|
|
30239
|
-
|
|
30239
|
+
Other reference connectors use this same pattern for partition dropdowns.
|
|
30240
30240
|
|
|
30241
30241
|
## Project Structure (Proven Pattern)
|
|
30242
30242
|
|
|
@@ -30266,7 +30266,7 @@ The F5 BIG-IP connector uses this same pattern for partition dropdowns.
|
|
|
30266
30266
|
\u2502 \u251C\u2500\u2500 discovery/ # Certificate discovery logic
|
|
30267
30267
|
\u2502 \u2502 \u251C\u2500\u2500 discovery.go # Main discovery orchestration
|
|
30268
30268
|
\u2502 \u2502 \u2514\u2500\u2500 types.go # Request/response types
|
|
30269
|
-
\u2502 \u2514\u2500\u2500 <target>/ # Target-specific logic (e.g.,
|
|
30269
|
+
\u2502 \u2514\u2500\u2500 <target>/ # Target-specific logic (e.g., webserver/, appliance/)
|
|
30270
30270
|
\u2502 \u251C\u2500\u2500 client.go # Connection client abstraction (SSH, API, etc.)
|
|
30271
30271
|
\u2502 \u251C\u2500\u2500 detect.go # Target software detection
|
|
30272
30272
|
\u2502 \u251C\u2500\u2500 install.go # Certificate file writing
|
|
@@ -30356,7 +30356,7 @@ func New() *fx.App {
|
|
|
30356
30356
|
var LESSONS_LEARNED = `
|
|
30357
30357
|
# Lessons Learned: Machine Connector Projects
|
|
30358
30358
|
|
|
30359
|
-
This document captures everything that worked well, everything that was challenging, and every mistake or pitfall encountered while building machine connectors
|
|
30359
|
+
This document captures everything that worked well, everything that was challenging, and every mistake or pitfall encountered while building machine connectors across SSH-based targets and REST API appliances. Use this to avoid repeating mistakes and to replicate what worked.
|
|
30360
30360
|
|
|
30361
30361
|
---
|
|
30362
30362
|
|
|
@@ -30461,7 +30461,7 @@ This is critical for any values that come from user input (file paths, passwords
|
|
|
30461
30461
|
|
|
30462
30462
|
**The reality**:
|
|
30463
30463
|
- Venafi sends certificates in the \`CertificateBundle\` as **DER-encoded binary** (\`[]byte\`)
|
|
30464
|
-
- Most target systems (
|
|
30464
|
+
- Most target systems (web servers, app servers) expect **PEM format** (text with BEGIN/END headers)
|
|
30465
30465
|
- Discovery must return certificates in **PEM format** (text strings)
|
|
30466
30466
|
|
|
30467
30467
|
**The fix**: Always use \`pem.EncodeToMemory()\` when converting from Venafi's DER format to files:
|
|
@@ -30581,7 +30581,7 @@ return c.String(http.StatusBadRequest, fmt.Sprintf("SSH connection failed: %s",
|
|
|
30581
30581
|
|
|
30582
30582
|
### 8. Config Parsing is Inherently Fragile
|
|
30583
30583
|
|
|
30584
|
-
**The issue**:
|
|
30584
|
+
**The issue**: The target's config files use an INI-like format, and our parser is basic:
|
|
30585
30585
|
|
|
30586
30586
|
\`\`\`go
|
|
30587
30587
|
func parseINI(content string) map[string]map[string]string {
|
|
@@ -30590,34 +30590,34 @@ func parseINI(content string) map[string]map[string]string {
|
|
|
30590
30590
|
\`\`\`
|
|
30591
30591
|
|
|
30592
30592
|
**What it doesn't handle**:
|
|
30593
|
-
- Multi-line values (rare
|
|
30593
|
+
- Multi-line values (rare but possible)
|
|
30594
30594
|
- Escaped characters
|
|
30595
30595
|
- Include directives
|
|
30596
30596
|
- App-level configs (only reads system/local and system/default)
|
|
30597
30597
|
|
|
30598
|
-
**Why it worked anyway**:
|
|
30598
|
+
**Why it worked anyway**: The target's SSL config is always simple key=value pairs. The basic parser handles 95%+ of real-world configs.
|
|
30599
30599
|
|
|
30600
30600
|
**Recommendation for the next connector**: Match the parser complexity to the target's config format. If the target uses JSON/YAML, use Go's standard \`encoding/json\` or a YAML library. Don't build custom parsers for well-known formats.
|
|
30601
30601
|
|
|
30602
30602
|
### 9. Container Detection Based on Image Name
|
|
30603
30603
|
|
|
30604
|
-
**The issue**: Container
|
|
30604
|
+
**The issue**: Container detection uses \`grep -i <product>\` on the image name:
|
|
30605
30605
|
|
|
30606
30606
|
\`\`\`go
|
|
30607
|
-
cs.RunCommand(client, fmt.Sprintf("%s ps --format '{{.Names}} {{.Image}}' | grep -i
|
|
30607
|
+
cs.RunCommand(client, fmt.Sprintf("%s ps --format '{{.Names}} {{.Image}}' | grep -i <product>", runtime))
|
|
30608
30608
|
\`\`\`
|
|
30609
30609
|
|
|
30610
|
-
**The problem**: This misses containers with custom image names that don't include
|
|
30610
|
+
**The problem**: This misses containers with custom image names that don't include the product name and may false-positive on unrelated containers that happen to match.
|
|
30611
30611
|
|
|
30612
30612
|
**Recommendation for next connector**: Be specific about what you're detecting. If the target software has a known process name, binary path, or config file, check for those inside the container rather than relying on image name.
|
|
30613
30613
|
|
|
30614
30614
|
### 10. GetTargetConfiguration Can Power Dynamic Dropdowns
|
|
30615
30615
|
|
|
30616
|
-
**The original issue**: The
|
|
30616
|
+
**The original issue**: The SSH-based connector returned an empty response from \`getTargetConfiguration\`.
|
|
30617
30617
|
|
|
30618
|
-
**Updated learning from
|
|
30618
|
+
**Updated learning from a REST API appliance connector**: \`getTargetConfiguration\` is NOT just a stub. It can return dynamic values that populate dropdown fields in the Venafi UI. This is done via \`x-targetConfigurationRef\` in the manifest.
|
|
30619
30619
|
|
|
30620
|
-
**Pattern**: When the target has scopes/partitions/
|
|
30620
|
+
**Pattern**: When the target has scopes/partitions/virtual domains, return them from \`getTargetConfiguration\`:
|
|
30621
30621
|
|
|
30622
30622
|
\`\`\`go
|
|
30623
30623
|
type GetTargetConfigurationResponse struct {
|
|
@@ -30646,7 +30646,7 @@ Then in the manifest, add a \`targetConfiguration\` domainSchema and reference t
|
|
|
30646
30646
|
}
|
|
30647
30647
|
\`\`\`
|
|
30648
30648
|
|
|
30649
|
-
This creates a dropdown in the keystore form populated with actual values from the target system.
|
|
30649
|
+
This creates a dropdown in the keystore form populated with actual values from the target system. Other reference connectors use this same pattern for partition dropdowns.
|
|
30650
30650
|
|
|
30651
30651
|
**Recommendation**: Use \`getTargetConfiguration\` whenever the target has enumerable scopes, profiles, or resource lists that users need to select from. Keep it as a stub only if there's nothing dynamic to populate.
|
|
30652
30652
|
|
|
@@ -30663,7 +30663,7 @@ This creates a dropdown in the keystore form populated with actual values from t
|
|
|
30663
30663
|
|
|
30664
30664
|
**The problem**: The Venafi platform silently rejects this format. Discovery appears to succeed but returns: "The webhook invocation for discovering certificates failed because the discovery result is empty."
|
|
30665
30665
|
|
|
30666
|
-
**The correct format** (confirmed by
|
|
30666
|
+
**The correct format** (confirmed by multiple reference connectors):
|
|
30667
30667
|
|
|
30668
30668
|
\`\`\`json
|
|
30669
30669
|
{
|
|
@@ -30688,9 +30688,9 @@ Key differences:
|
|
|
30688
30688
|
|
|
30689
30689
|
**Why this is so dangerous**: The error message gives no clue about the wrong format. You'll spend hours debugging connectivity when the real issue is JSON structure. Always verify your response matches the types in the \`discovery-types.go\` template.
|
|
30690
30690
|
|
|
30691
|
-
### 12. Multi-Scope Discovery Pattern (
|
|
30691
|
+
### 12. Multi-Scope Discovery Pattern (Virtual Domains, Partitions, Tenants)
|
|
30692
30692
|
|
|
30693
|
-
**The pattern**: Many network appliances have scoping concepts (
|
|
30693
|
+
**The pattern**: Many network appliances have scoping concepts (virtual domains, partitions, tenants). Discovery should support both "discover all" and "discover specific scope."
|
|
30694
30694
|
|
|
30695
30695
|
**Implementation**:
|
|
30696
30696
|
1. The discovery request includes a scope field (e.g., \`vdom\`)
|
|
@@ -30737,7 +30737,7 @@ With localization: \`"vdomLabel": "VDOM (leave blank to discover all)"\`
|
|
|
30737
30737
|
**What actually happens on network appliances**: Certificate data is split across APIs:
|
|
30738
30738
|
- **Operational/Monitor APIs** return rich metadata (subject, issuer, serial number, fingerprint, validity dates) but NOT the PEM certificate content
|
|
30739
30739
|
- **Configuration/CMDB APIs** return the actual PEM content, but only when fetching individual certificates \u2014 **batch/list endpoints strip heavy blob fields** to keep responses small
|
|
30740
|
-
- This split is common across
|
|
30740
|
+
- This split is common across REST API appliances (e.g., CMDB vs Monitor endpoints, mgmt vs stats endpoints)
|
|
30741
30741
|
|
|
30742
30742
|
**The symptom in Venafi**: Discovery succeeds with no errors from the connector, but the Venafi Cloud activity log shows: **"Discovered certificate cannot be read"** for every certificate. This means the \`certificate\` field in the discovery response is empty or not valid PEM. The platform accepted the \`messages\` format but couldn't parse the cert data inside.
|
|
30743
30743
|
|
|
@@ -30773,7 +30773,7 @@ for _, cert := range certDetails {
|
|
|
30773
30773
|
|
|
30774
30774
|
**Two approaches based on target type:**
|
|
30775
30775
|
|
|
30776
|
-
**Approach A \u2014 Service-type filtering (
|
|
30776
|
+
**Approach A \u2014 Service-type filtering (network appliance pattern):** For network appliances where certificates are assigned to different service types (admin HTTPS, SSL VPN, IPsec VPN, VIPs, SSL inspection), the \`discoveryTypes\` match the appliance's service categories:
|
|
30777
30777
|
|
|
30778
30778
|
\`\`\`json
|
|
30779
30779
|
"oneOf": [
|
|
@@ -30787,7 +30787,7 @@ for _, cert := range certDetails {
|
|
|
30787
30787
|
|
|
30788
30788
|
These map 1:1 to the binding types. The connector queries only the selected service endpoints.
|
|
30789
30789
|
|
|
30790
|
-
**Approach B \u2014 Usage-based filtering (
|
|
30790
|
+
**Approach B \u2014 Usage-based filtering (API management platform pattern):** For API management platforms where certificates serve different infrastructure roles, the \`discoveryTypes\` match certificate usage categories that the admin would recognize from their platform:
|
|
30791
30791
|
|
|
30792
30792
|
\`\`\`json
|
|
30793
30793
|
"oneOf": [
|
|
@@ -30800,7 +30800,7 @@ These map 1:1 to the binding types. The connector queries only the selected serv
|
|
|
30800
30800
|
|
|
30801
30801
|
Here, the first three types all come from the same data source (keystores), but are classified after enumeration based on how they're used. The fourth comes from a separate data source (consumer apps). This approach replaces a less intuitive "profileBoundOnly" toggle \u2014 unchecking "Unassigned Keystores" achieves the same filtering in platform-native terms.
|
|
30802
30802
|
|
|
30803
|
-
**Key: choosing the right categories.** Ask: "How would an admin of this platform describe their certificates?" For
|
|
30803
|
+
**Key: choosing the right categories.** Ask: "How would an admin of this platform describe their certificates?" For a network appliance, it's by service type (VPN, firewall, management). For an API management platform, it's by infrastructure role (gateway, backend, unassigned, consumer). For a load balancer, it might be by partition or virtual server type. The checkboxes should match the admin's mental model.
|
|
30804
30804
|
|
|
30805
30805
|
**Implementation \u2014 binding metadata with per-type dropdown fields:**
|
|
30806
30806
|
|
|
@@ -30902,9 +30902,9 @@ Reporting configuration-level TLS settings (what the platform *says* it supports
|
|
|
30902
30902
|
- Regular fields (no readOnly) \u2014 same result: stored but invisible in the UI
|
|
30903
30903
|
- Fields added to \`x-primaryKey\` \u2014 **visible** in the UI immediately
|
|
30904
30904
|
|
|
30905
|
-
**The rule**: The Venafi UI installation detail view renders ONLY the fields listed in the manifest's \`x-primaryKey\` arrays for both keystore and binding sections. No exceptions. No production connector
|
|
30905
|
+
**The rule**: The Venafi UI installation detail view renders ONLY the fields listed in the manifest's \`x-primaryKey\` arrays for both keystore and binding sections. No exceptions. No production connector uses \`readOnly: true\` \u2014 all visible metadata goes through \`x-primaryKey\`.
|
|
30906
30906
|
|
|
30907
|
-
**Verification**:
|
|
30907
|
+
**Verification**: Reference connectors show \`certificateName\`, \`vdom\`, \`bindingType\`, and \`targetName\` in the UI \u2014 every one of them is in \`x-primaryKey\`. Fields not in \`x-primaryKey\` are never shown regardless of any other manifest attributes.
|
|
30908
30908
|
|
|
30909
30909
|
**What administrators actually want**: When a platform admin looks at Venafi, they want to see the same context they see in their platform admin UI:
|
|
30910
30910
|
- **Certificate usage**: "api-gateway" vs "outbound-mtls" vs "keystore-only"
|
|
@@ -30941,7 +30941,7 @@ Reporting configuration-level TLS settings (what the platform *says* it supports
|
|
|
30941
30941
|
**What actually happens with readOnly fields**:
|
|
30942
30942
|
- The Venafi platform **stores** the data correctly (confirmed via REST API queries)
|
|
30943
30943
|
- The Venafi UI **does NOT display** readOnly fields in the installation detail view
|
|
30944
|
-
- No production machine connector
|
|
30944
|
+
- No production machine connector uses \`readOnly: true\`
|
|
30945
30945
|
- The UI only renders fields listed in \`x-primaryKey\` (see section 16)
|
|
30946
30946
|
|
|
30947
30947
|
**When readOnly might be useful**: Only for fields that are genuinely for internal use \u2014 like a platform UUID (\`keystoreId\`) used by the connector code during provisioning to target the correct resource, but not needed by human users in the UI. Even then, consider whether the field is worth including at all if users can't see it.
|
|
@@ -31042,7 +31042,7 @@ if binding.ApiNames == "" {
|
|
|
31042
31042
|
4. Cross-reference with connector's discovery response \u2014 find MIs where a primaryKey field is empty
|
|
31043
31043
|
5. Add a default value for the empty field, rebuild, and re-deploy
|
|
31044
31044
|
|
|
31045
|
-
**Proven defaults from
|
|
31045
|
+
**Proven defaults from an API management platform connector**:
|
|
31046
31046
|
- \`catalogName\`: "org-level" for keystores/profiles not deployed to any catalog
|
|
31047
31047
|
- \`productName\`: "N/A" for catalogs without published products
|
|
31048
31048
|
- \`apiNames\`: "N/A" for catalogs without published APIs
|
|
@@ -31060,7 +31060,7 @@ if binding.ApiNames == "" {
|
|
|
31060
31060
|
|
|
31061
31061
|
With catalog-level enrichment, BOTH entries show: Product = "Product Discovery, Customer Platform", APIs = "Product Catalog API, Customer API, Order API". This is misleading and unhelpful.
|
|
31062
31062
|
|
|
31063
|
-
**The fix \u2014 Profile-level enrichment via title matching**: When the target platform doesn't provide a direct API-to-profile mapping (e.g.,
|
|
31063
|
+
**The fix \u2014 Profile-level enrichment via title matching**: When the target platform doesn't provide a direct API-to-profile mapping (e.g., the platform's \`tls_client_profile_urls\` is empty on published APIs), use the TLS profile title as a heuristic to match against product names. Platform administrators typically follow naming conventions where profile titles encode the product/service context:
|
|
31064
31064
|
|
|
31065
31065
|
\`\`\`go
|
|
31066
31066
|
// matchProfileToProduct matches a TLS profile to a specific product
|
|
@@ -31102,7 +31102,7 @@ func matchProfileToProduct(profileTitle string, cp catalogProductInfo) (string,
|
|
|
31102
31102
|
- **\`discoveryTypes\` (array field with oneOf checkboxes)**: \`x-labelLocalizationKey\` is NOT resolved \u2014 the raw key text appears as the label in the UI (e.g., "discovery.typesLabel" shows literally). You MUST use the \`"title"\` property instead.
|
|
31103
31103
|
- **Boolean discovery fields** (e.g., \`excludeExpiredCertificates\`): \`x-labelLocalizationKey\` DOES work, but ONLY with the nested \`"discovery.xxx"\` pattern.
|
|
31104
31104
|
|
|
31105
|
-
**The correct pattern (confirmed
|
|
31105
|
+
**The correct pattern (confirmed across multiple connectors):**
|
|
31106
31106
|
|
|
31107
31107
|
\`\`\`json
|
|
31108
31108
|
"discovery": {
|
|
@@ -31207,11 +31207,11 @@ for _, chainDER := range bundle.CertificateChain {
|
|
|
31207
31207
|
2. Is the root CA ever included, or only intermediates?
|
|
31208
31208
|
3. Should connectors assume the order and pass through, or re-sort based on issuer/subject matching?
|
|
31209
31209
|
|
|
31210
|
-
This matters for targets like
|
|
31210
|
+
This matters for targets like load balancers, ADCs, and Java keystores where incorrect chain order causes TLS handshake failures. Until documented, connectors should pass through in the order received and note this assumption in their documentation.
|
|
31211
31211
|
|
|
31212
31212
|
### 25. DER-to-PEM Conversion for REST API Discovery
|
|
31213
31213
|
|
|
31214
|
-
**The pattern**: When a REST API target returns certificates as base64-encoded DER (common for
|
|
31214
|
+
**The pattern**: When a REST API target returns certificates as base64-encoded DER (common for REST API appliances), the discovery response must convert to PEM. This conversion is easy to miss because the API response looks like a base64 string that could be passed through directly.
|
|
31215
31215
|
|
|
31216
31216
|
\`\`\`go
|
|
31217
31217
|
// REST API returns base64 DER \u2014 MUST convert to PEM for discovery response
|
|
@@ -31309,7 +31309,7 @@ Getting these wrong can cause the target service to refuse to start.
|
|
|
31309
31309
|
|
|
31310
31310
|
### 1. Unit Tests
|
|
31311
31311
|
|
|
31312
|
-
The
|
|
31312
|
+
The SSH-based connector has zero unit tests. This was acceptable for an MVP but should be addressed from the start:
|
|
31313
31313
|
|
|
31314
31314
|
- Mock \`ClientServices\` to test handlers without SSH
|
|
31315
31315
|
- Test certificate parsing with known PEM files
|
|
@@ -31357,13 +31357,13 @@ Or return \`result: false\` with a helpful message if the target software isn't
|
|
|
31357
31357
|
|
|
31358
31358
|
---
|
|
31359
31359
|
|
|
31360
|
-
## REST API Appliance Patterns
|
|
31360
|
+
## REST API Appliance Patterns
|
|
31361
31361
|
|
|
31362
31362
|
These patterns apply to machine connectors that communicate via REST API rather than SSH.
|
|
31363
31363
|
|
|
31364
31364
|
### 20. PKCS12 Is the Dominant Provisioning Format for REST API Targets
|
|
31365
31365
|
|
|
31366
|
-
**The pattern**: Most REST API appliances
|
|
31366
|
+
**The pattern**: Most REST API appliances accept certificate bundles as **PKCS12** files, not raw PEM files. The connector must build a PKCS12 bundle from Venafi's DER-encoded certificate bundle.
|
|
31367
31367
|
|
|
31368
31368
|
**Implementation** (using \`go-pkcs12\`):
|
|
31369
31369
|
|
|
@@ -31410,7 +31410,7 @@ func parsePrivateKey(keyDER []byte) (crypto.PrivateKey, error) {
|
|
|
31410
31410
|
}
|
|
31411
31411
|
\`\`\`
|
|
31412
31412
|
|
|
31413
|
-
This sequence is proven across
|
|
31413
|
+
This sequence is proven across multiple REST API appliance connectors. Never assume RSA.
|
|
31414
31414
|
|
|
31415
31415
|
### 22. CRITICAL: Binding Must Never Be Nil in Discovery Results
|
|
31416
31416
|
|
|
@@ -31446,7 +31446,7 @@ if hasBindings {
|
|
|
31446
31446
|
2. **If yes**: Upload a new cert with a unique name (e.g., \`certName_YYMMDD_serial\`), update the binding to point to the new cert, then optionally delete the old cert
|
|
31447
31447
|
3. **If no**: Simply upload the cert and create the binding reference
|
|
31448
31448
|
|
|
31449
|
-
This is the proven pattern for
|
|
31449
|
+
This is the proven pattern for appliances where certificates are immutable once created. Some API management platforms handle this differently \u2014 their PATCH API preserves UUIDs when updating keystore content in-place.
|
|
31450
31450
|
|
|
31451
31451
|
**Key**: The \`installCertificateBundle\` handler returns the updated keystore (with the new cert name), which is passed to \`configureInstallationEndpoint\`. Use the keystore fields to carry the cert name between activities.
|
|
31452
31452
|
|
|
@@ -31490,9 +31490,9 @@ if pageEnd < len(allCerts) {
|
|
|
31490
31490
|
// page = nil means "done"
|
|
31491
31491
|
\`\`\`
|
|
31492
31492
|
|
|
31493
|
-
### 25. OAuth2 Token Exchange (
|
|
31493
|
+
### 25. OAuth2 Token Exchange (API Management Platform Pattern)
|
|
31494
31494
|
|
|
31495
|
-
**The pattern**: Some REST API targets (
|
|
31495
|
+
**The pattern**: Some REST API targets (API management platforms) use OAuth2 token exchange instead of static API keys. The connector exchanges credentials for a bearer token at the start of each endpoint call.
|
|
31496
31496
|
|
|
31497
31497
|
\`\`\`go
|
|
31498
31498
|
func (h *Handler) authenticate() error {
|
|
@@ -31516,34 +31516,34 @@ func (h *Handler) authenticate() error {
|
|
|
31516
31516
|
\`\`\`
|
|
31517
31517
|
|
|
31518
31518
|
**IMPORTANT \u2014 Token endpoint body format varies by target**:
|
|
31519
|
-
- **
|
|
31519
|
+
- **Some API platforms**: Require \`application/json\` body (NOT form-urlencoded). Sending form-urlencoded silently fails.
|
|
31520
31520
|
- **Standard OAuth2**: Uses \`application/x-www-form-urlencoded\` per RFC 6749 (use \`SetFormData()\`).
|
|
31521
31521
|
- Always check the target API documentation for the expected content type.
|
|
31522
31522
|
|
|
31523
|
-
**Key**: Token has a TTL (8 hours
|
|
31523
|
+
**Key**: Token has a TTL (e.g., 8 hours). For connectors where each endpoint call is stateless, exchange the token at the start of each call \u2014 don't cache across calls.
|
|
31524
31524
|
|
|
31525
31525
|
### 26. REST API Targets May Use PATCH Instead of PUT
|
|
31526
31526
|
|
|
31527
|
-
**The learning**:
|
|
31527
|
+
**The learning**: Some API management platforms return 405 (Method Not Allowed) for PUT on keystore updates and require PATCH instead. Other REST API appliances require PUT for most updates.
|
|
31528
31528
|
|
|
31529
31529
|
**The fix**: Don't assume PUT or PATCH \u2014 check the target API documentation and test both during development. Make the HTTP method part of your helper function:
|
|
31530
31530
|
|
|
31531
31531
|
\`\`\`go
|
|
31532
31532
|
func (h *Handler) UpdateResource(path string, body interface{}) (*resty.Response, error) {
|
|
31533
|
-
//
|
|
31533
|
+
// some targets require PATCH; others use PUT
|
|
31534
31534
|
return h.client.R().SetBody(body).Patch(h.baseURL + path) // or .Put()
|
|
31535
31535
|
}
|
|
31536
31536
|
\`\`\`
|
|
31537
31537
|
|
|
31538
31538
|
### 27. Protocols Field Must Never Be Empty Array for Machine Identities
|
|
31539
31539
|
|
|
31540
|
-
**The learning
|
|
31540
|
+
**The learning**: Venafi platform silently drops machine identities with \`protocols: []\` (empty array) in the discovery response. Omitting \`protocols\` entirely (with \`omitempty\`) causes hard discovery failure.
|
|
31541
31541
|
|
|
31542
31542
|
**The fix**: For onboard discovery, do NOT set protocols at all. Leave the field out of the response struct entirely (don't even define it). Protocols are for network discovery connectors that observe live TLS connections, not for onboard discovery that reads platform inventory.
|
|
31543
31543
|
|
|
31544
31544
|
### 28. Null-Safe JSON Arrays in Discovery Responses
|
|
31545
31545
|
|
|
31546
|
-
**The learning
|
|
31546
|
+
**The learning**: Go's \`encoding/json\` marshals a nil slice as \`null\`, but the Venafi platform expects empty arrays to be \`[]\` (not \`null\`). A \`null\` value for \`certificateChain\` or \`machineIdentities\` can cause discovery failures or silent data loss.
|
|
31547
31547
|
|
|
31548
31548
|
**The symptom**: Discovery responses with \`"certificateChain": null\` or \`"machineIdentities": null\` are silently rejected or produce incomplete results.
|
|
31549
31549
|
|
|
@@ -31660,7 +31660,7 @@ func truncateMetadata(s string, totalCount int) string {
|
|
|
31660
31660
|
|
|
31661
31661
|
**The mistake**: Using modern PKCS12 encoding (AES-256-CBC + SHA-256) when building PKCS12 bundles for certificate import to network appliances.
|
|
31662
31662
|
|
|
31663
|
-
**The problem**: Many network appliances
|
|
31663
|
+
**The problem**: Many network appliances cannot parse modern PKCS12 encoding. The import API returns success or a vague error, but the certificate is not usable. These appliances specifically require the legacy PKCS12 format (3DES + SHA1).
|
|
31664
31664
|
|
|
31665
31665
|
**The fix**: When building PKCS12 bundles in Go, use the Legacy encoder:
|
|
31666
31666
|
|
|
@@ -31670,7 +31670,7 @@ import gopkcs12 "software.sslmate.com/src/go-pkcs12"
|
|
|
31670
31670
|
// CORRECT: Legacy encoding (3DES/SHA1) \u2014 compatible with network appliances
|
|
31671
31671
|
p12Data, err := gopkcs12.Legacy.Encode(key, cert, caCerts, password)
|
|
31672
31672
|
|
|
31673
|
-
// WRONG: Modern encoding (AES-256/SHA-256) \u2014 rejected by
|
|
31673
|
+
// WRONG: Modern encoding (AES-256/SHA-256) \u2014 rejected by many network appliances
|
|
31674
31674
|
// p12Data, err := gopkcs12.Modern.Encode(key, cert, caCerts, password)
|
|
31675
31675
|
\`\`\`
|
|
31676
31676
|
|
|
@@ -31911,7 +31911,7 @@ if _, err := x509.ParsePKCS8PrivateKey(keyDER); err == nil {
|
|
|
31911
31911
|
|
|
31912
31912
|
**When to use which approach**:
|
|
31913
31913
|
- **Target accepts PEM** (GCP, AWS, cloud APIs): Use the no-re-marshal approach above. Just detect the type for the PEM header and wrap the original DER bytes.
|
|
31914
|
-
- **Target requires PKCS12** (
|
|
31914
|
+
- **Target requires PKCS12** (network appliances): You MUST parse into Go types to pass to \`pkcs12.Encode()\`. Re-marshaling is unavoidable and acceptable here because the PKCS12 encoder produces its own encoding.
|
|
31915
31915
|
|
|
31916
31916
|
### 42. CRITICAL: Handle Both DER and PEM Input in Certificate Bundle
|
|
31917
31917
|
|
|
@@ -32671,8 +32671,8 @@ var TEMPLATES = {
|
|
|
32671
32671
|
"// File: internal/app/domain/binding.go",
|
|
32672
32672
|
"// CUSTOMIZE: Replace fields with your target-specific service data.",
|
|
32673
32673
|
"// Choose the pattern that fits your target:",
|
|
32674
|
-
"// SSH-based targets: ServiceType + ServicePort (e.g.,
|
|
32675
|
-
"// REST API appliances: BindingType + TargetName (e.g.,
|
|
32674
|
+
"// SSH-based targets: ServiceType + ServicePort (e.g., web server, app server)",
|
|
32675
|
+
"// REST API appliances: BindingType + TargetName (e.g., load balancer, firewall, ADC)",
|
|
32676
32676
|
"// ============================================================",
|
|
32677
32677
|
"",
|
|
32678
32678
|
"package domain",
|
|
@@ -32685,7 +32685,7 @@ var TEMPLATES = {
|
|
|
32685
32685
|
'// ServicePort int `json:"servicePort"` // port the service listens on',
|
|
32686
32686
|
"// }",
|
|
32687
32687
|
"",
|
|
32688
|
-
"// --- Option B: REST API appliance binding (
|
|
32688
|
+
"// --- Option B: REST API appliance binding (network appliance pattern) ---",
|
|
32689
32689
|
"",
|
|
32690
32690
|
"// Binding represents how a certificate is used by a target service.",
|
|
32691
32691
|
'// BindingType identifies the service category (e.g., "admin-https", "ssl-vpn",',
|
|
@@ -32702,17 +32702,49 @@ var TEMPLATES = {
|
|
|
32702
32702
|
"// ============================================================",
|
|
32703
32703
|
"// File: internal/app/domain/certificate_bundle.go",
|
|
32704
32704
|
"// DO NOT MODIFY: This is standard across all connectors.",
|
|
32705
|
+
"//",
|
|
32706
|
+
'// IMPORTANT: The manifest schema uses "contentEncoding": "base64" and',
|
|
32707
|
+
'// "x-encrypted-base64": true on the privateKey field.',
|
|
32708
|
+
"// Venafi Cloud sends certificateBundle fields as base64-encoded DER.",
|
|
32709
|
+
"//",
|
|
32710
|
+
"// Choose ONE option below:",
|
|
32711
|
+
"//",
|
|
32712
|
+
"// Option A (RECOMMENDED for REST API connectors):",
|
|
32713
|
+
"// Use string fields and explicitly base64.StdEncoding.DecodeString() each field.",
|
|
32714
|
+
"// This is the most explicit pattern and proven across multiple connector builds.",
|
|
32715
|
+
"// After decoding, you have DER bytes \u2014 convert to PEM with pem.EncodeToMemory().",
|
|
32716
|
+
"//",
|
|
32717
|
+
"// Option B (works for SSH connectors):",
|
|
32718
|
+
"// Use []byte fields \u2014 Go's JSON unmarshaler auto-decodes base64 into []byte.",
|
|
32719
|
+
"// After unmarshaling, fields already contain DER bytes.",
|
|
32720
|
+
"//",
|
|
32721
|
+
"// Both options produce the same result: DER-encoded binary data.",
|
|
32705
32722
|
"// ============================================================",
|
|
32706
32723
|
"",
|
|
32707
32724
|
"package domain",
|
|
32708
32725
|
"",
|
|
32709
32726
|
"// CertificateBundle represents the certificate data sent by Venafi for provisioning.",
|
|
32727
|
+
"//",
|
|
32728
|
+
"// Option A: string fields (RECOMMENDED \u2014 explicit base64 decode)",
|
|
32729
|
+
"// certDER, _ := base64.StdEncoding.DecodeString(bundle.Certificate)",
|
|
32730
|
+
"// keyDER, _ := base64.StdEncoding.DecodeString(bundle.PrivateKey)",
|
|
32731
|
+
"// for _, chainEntry := range bundle.CertificateChain {",
|
|
32732
|
+
"// chainDER, _ := base64.StdEncoding.DecodeString(chainEntry)",
|
|
32733
|
+
'// chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: chainDER})...)',
|
|
32734
|
+
"// }",
|
|
32710
32735
|
"type CertificateBundle struct {",
|
|
32711
|
-
' Certificate
|
|
32712
|
-
' PrivateKey
|
|
32713
|
-
' CertificateChain []
|
|
32736
|
+
' Certificate string `json:"certificate"`',
|
|
32737
|
+
' PrivateKey string `json:"privateKey"`',
|
|
32738
|
+
' CertificateChain []string `json:"certificateChain"`',
|
|
32714
32739
|
"}",
|
|
32715
32740
|
"",
|
|
32741
|
+
"// Option B: []byte fields (Go auto-decodes base64 \u2014 fields contain DER bytes directly)",
|
|
32742
|
+
"// type CertificateBundle struct {",
|
|
32743
|
+
'// Certificate []byte `json:"certificate"`',
|
|
32744
|
+
'// PrivateKey []byte `json:"privateKey"`',
|
|
32745
|
+
'// CertificateChain [][]byte `json:"certificateChain"`',
|
|
32746
|
+
"// }",
|
|
32747
|
+
"",
|
|
32716
32748
|
"// ============================================================",
|
|
32717
32749
|
"// File: internal/app/domain/client.go",
|
|
32718
32750
|
"// This is the same across all SSH-based connectors.",
|
|
@@ -32845,6 +32877,20 @@ var TEMPLATES = {
|
|
|
32845
32877
|
content: [
|
|
32846
32878
|
"// TEMPLATE: Discovery request/response types. Standard across all connectors.",
|
|
32847
32879
|
"// File: internal/app/discovery/types.go",
|
|
32880
|
+
"//",
|
|
32881
|
+
"// CRITICAL RULES:",
|
|
32882
|
+
'// 1. The response top-level key MUST be "messages" \u2014 the platform silently rejects other names.',
|
|
32883
|
+
"// 2. Use VALUE types (not pointers) for Keystore, Binding, and slice elements.",
|
|
32884
|
+
"// Pointers serialize to null when nil, which the platform may reject.",
|
|
32885
|
+
"// 3. Initialize all slices with make() to ensure JSON [] not null.",
|
|
32886
|
+
"// 4. The certificate field must contain a PEM string (not DER, not base64).",
|
|
32887
|
+
"// Always normalize through pem.EncodeToMemory() before returning \u2014 even if",
|
|
32888
|
+
"// the source is already PEM \u2014 to guarantee trailing newline and line wrapping.",
|
|
32889
|
+
'// 5. discoveryPage: null in the response signals "done" to the platform.',
|
|
32890
|
+
'// 6. Each DiscoveredCertificate MUST include an "installations" array with at least',
|
|
32891
|
+
"// one entry containing hostname, ipAddress, and port. Without this field, the",
|
|
32892
|
+
"// platform silently discards discovered certificates \u2014 no error, discovery shows",
|
|
32893
|
+
'// as "complete" but no certificates appear in the UI.',
|
|
32848
32894
|
"",
|
|
32849
32895
|
"package discovery",
|
|
32850
32896
|
"",
|
|
@@ -32860,11 +32906,10 @@ var TEMPLATES = {
|
|
|
32860
32906
|
"type DiscoverCertificatesConfiguration struct {",
|
|
32861
32907
|
" // DiscoveryTypes lets users toggle which service types to discover.",
|
|
32862
32908
|
" // For REST API appliances, this filters which binding endpoints are queried.",
|
|
32863
|
-
' // Example values: "admin-https", "ssl-vpn", "ipsec-vpn", "vip", "ssl-inspection"',
|
|
32864
32909
|
' // An empty slice means "discover all types".',
|
|
32865
32910
|
' DiscoveryTypes []string `json:"discoveryTypes"`',
|
|
32866
32911
|
' ExcludeExpiredCertificates bool `json:"excludeExpiredCertificates"`',
|
|
32867
|
-
" // Scope field for multi-tenant targets (e.g.,
|
|
32912
|
+
" // Scope field for multi-tenant targets (e.g., partition, tenant, virtual domain).",
|
|
32868
32913
|
" // Leave blank to auto-discover all scopes.",
|
|
32869
32914
|
' Scope string `json:"scope"`',
|
|
32870
32915
|
"}",
|
|
@@ -32878,29 +32923,82 @@ var TEMPLATES = {
|
|
|
32878
32923
|
"}",
|
|
32879
32924
|
"",
|
|
32880
32925
|
"// DiscoveryPage represents the current pagination state.",
|
|
32926
|
+
'// Return nil to signal "discovery complete" to the platform.',
|
|
32927
|
+
"// Return a non-nil DiscoveryPage to request another page.",
|
|
32881
32928
|
"type DiscoveryPage struct {",
|
|
32882
|
-
' DiscoveryType
|
|
32883
|
-
' Paginator string
|
|
32929
|
+
' DiscoveryType string `json:"discoveryType"`',
|
|
32930
|
+
' Paginator string `json:"paginator"`',
|
|
32884
32931
|
"}",
|
|
32885
32932
|
"",
|
|
32886
32933
|
"// DiscoverCertificatesResponse represents the response to a discovery request.",
|
|
32934
|
+
'// CRITICAL: The top-level JSON key MUST be "messages" \u2014 the platform ignores other keys.',
|
|
32887
32935
|
"type DiscoverCertificatesResponse struct {",
|
|
32936
|
+
' Messages []DiscoveredCertificate `json:"messages"`',
|
|
32888
32937
|
' Page *DiscoveryPage `json:"discoveryPage"`',
|
|
32889
|
-
' Messages []*DiscoveredCertificate `json:"messages"`',
|
|
32890
32938
|
"}",
|
|
32891
32939
|
"",
|
|
32892
32940
|
"// DiscoveredCertificate represents a single certificate found during discovery.",
|
|
32941
|
+
"// CRITICAL: Use VALUE types (not pointers) \u2014 pointers serialize to null which the platform rejects.",
|
|
32942
|
+
"// CRITICAL: The Installations field is REQUIRED \u2014 without it, the platform silently discards",
|
|
32943
|
+
'// the certificate. Discovery will show as "complete" but no certs appear in the UI.',
|
|
32893
32944
|
"type DiscoveredCertificate struct {",
|
|
32894
|
-
' Certificate string
|
|
32895
|
-
' CertificateChain []string
|
|
32896
|
-
' MachineIdentities []
|
|
32945
|
+
' Certificate string `json:"certificate"` // PEM string (use pem.EncodeToMemory)',
|
|
32946
|
+
' CertificateChain []string `json:"certificateChain"` // PEM strings, initialize with make()',
|
|
32947
|
+
' MachineIdentities []MachineIdentity `json:"machineIdentities"` // VALUE type slice, initialize with make()',
|
|
32948
|
+
' Installations []Installation `json:"installations"` // REQUIRED \u2014 at least one entry',
|
|
32949
|
+
"}",
|
|
32950
|
+
"",
|
|
32951
|
+
"// Installation identifies where the certificate was found (host + port).",
|
|
32952
|
+
"// CRITICAL: At least one Installation is required per DiscoveredCertificate.",
|
|
32953
|
+
"// Without this field, the platform silently drops the certificate from results.",
|
|
32954
|
+
"type Installation struct {",
|
|
32955
|
+
' Hostname string `json:"hostname"`',
|
|
32956
|
+
' IPAddress string `json:"ipAddress"`',
|
|
32957
|
+
' Port int `json:"port"`',
|
|
32897
32958
|
"}",
|
|
32898
32959
|
"",
|
|
32899
32960
|
"// MachineIdentity represents a certificate usage found during discovery.",
|
|
32961
|
+
"// CRITICAL: Use VALUE types for Keystore and Binding \u2014 never pointers.",
|
|
32962
|
+
'// Binding must NEVER be empty \u2014 use a default like "unbound" for certs with no bindings.',
|
|
32963
|
+
"// All x-primaryKey fields must have non-empty values or the identity is silently dropped.",
|
|
32900
32964
|
"type MachineIdentity struct {",
|
|
32901
|
-
' Keystore
|
|
32902
|
-
' Binding
|
|
32903
|
-
"}"
|
|
32965
|
+
' Keystore domain.Keystore `json:"keystore"`',
|
|
32966
|
+
' Binding domain.Binding `json:"binding"`',
|
|
32967
|
+
"}",
|
|
32968
|
+
"",
|
|
32969
|
+
"// ============================================================",
|
|
32970
|
+
"// Example: Building a discovery response",
|
|
32971
|
+
"// ============================================================",
|
|
32972
|
+
"//",
|
|
32973
|
+
"// messages := make([]DiscoveredCertificate, 0) // MUST use make() \u2014 nil serializes as null",
|
|
32974
|
+
"// chainPEMs := make([]string, 0) // same for chains",
|
|
32975
|
+
"//",
|
|
32976
|
+
"// // Always normalize PEM through pem.EncodeToMemory, even if source is already PEM:",
|
|
32977
|
+
"// block, _ := pem.Decode([]byte(rawPEM))",
|
|
32978
|
+
"// normalizedPEM := string(pem.EncodeToMemory(block))",
|
|
32979
|
+
"//",
|
|
32980
|
+
"// // For REST API targets returning base64 DER:",
|
|
32981
|
+
"// derBytes, _ := base64.StdEncoding.DecodeString(apiResponse.Base64)",
|
|
32982
|
+
'// certPEM := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}))',
|
|
32983
|
+
"//",
|
|
32984
|
+
"// messages = append(messages, DiscoveredCertificate{",
|
|
32985
|
+
"// Certificate: normalizedPEM,",
|
|
32986
|
+
"// CertificateChain: chainPEMs,",
|
|
32987
|
+
"// MachineIdentities: []MachineIdentity{{",
|
|
32988
|
+
'// Keystore: domain.Keystore{CertificateName: "my-cert"},',
|
|
32989
|
+
'// Binding: domain.Binding{BindingType: "unbound"}, // never empty',
|
|
32990
|
+
"// }},",
|
|
32991
|
+
"// Installations: []Installation{{",
|
|
32992
|
+
"// Hostname: connection.HostnameOrAddress,",
|
|
32993
|
+
"// IPAddress: connection.HostnameOrAddress,",
|
|
32994
|
+
"// Port: connection.Port,",
|
|
32995
|
+
"// }}, // REQUIRED \u2014 without this, certs are silently dropped",
|
|
32996
|
+
"// })",
|
|
32997
|
+
"//",
|
|
32998
|
+
"// return c.JSON(http.StatusOK, DiscoverCertificatesResponse{",
|
|
32999
|
+
"// Messages: messages,",
|
|
33000
|
+
"// Page: nil, // nil = done; non-nil = call again",
|
|
33001
|
+
"// })"
|
|
32904
33002
|
].join("\n"),
|
|
32905
33003
|
customizable: true
|
|
32906
33004
|
},
|
|
@@ -32910,12 +33008,14 @@ var TEMPLATES = {
|
|
|
32910
33008
|
targetPath: "internal/app/<target>/install_helpers.go",
|
|
32911
33009
|
content: [
|
|
32912
33010
|
"// TEMPLATE: Helper functions for certificate installation.",
|
|
32913
|
-
"// These are proven patterns from
|
|
33011
|
+
"// These are proven patterns from multiple connector builds.",
|
|
32914
33012
|
"// Include these in your install.go or a helpers.go file.",
|
|
32915
33013
|
"",
|
|
32916
33014
|
"package <target>",
|
|
32917
33015
|
"",
|
|
32918
33016
|
"import (",
|
|
33017
|
+
' "crypto/ecdsa"',
|
|
33018
|
+
' "crypto/rsa"',
|
|
32919
33019
|
' "crypto/x509"',
|
|
32920
33020
|
' "encoding/pem"',
|
|
32921
33021
|
' "fmt"',
|
|
@@ -32925,27 +33025,52 @@ var TEMPLATES = {
|
|
|
32925
33025
|
' "go.uber.org/zap"',
|
|
32926
33026
|
")",
|
|
32927
33027
|
"",
|
|
32928
|
-
"//
|
|
32929
|
-
|
|
32930
|
-
"
|
|
32931
|
-
"
|
|
32932
|
-
"
|
|
32933
|
-
'
|
|
33028
|
+
"// encodeKeyToPEM detects the key type and encodes to PEM with the correct header.",
|
|
33029
|
+
"//",
|
|
33030
|
+
"// CRITICAL: When PKCS8 wraps a key, re-marshal to the native format.",
|
|
33031
|
+
"// Some targets validate that the DER encoding matches the PEM header.",
|
|
33032
|
+
'// PKCS8 DER with "RSA PRIVATE KEY" header = INVALID (DER is PKCS8, header says PKCS1).',
|
|
33033
|
+
'// PKCS1 DER with "RSA PRIVATE KEY" header = VALID (both agree on format).',
|
|
33034
|
+
"//",
|
|
33035
|
+
'// Do NOT hardcode "RSA PRIVATE KEY" \u2014 Venafi may issue EC or other key types.',
|
|
33036
|
+
"func encodeKeyToPEM(keyDER []byte) []byte {",
|
|
33037
|
+
" // Try PKCS8 first (most common from Venafi)",
|
|
33038
|
+
" key, err := x509.ParsePKCS8PrivateKey(keyDER)",
|
|
33039
|
+
" if err == nil {",
|
|
33040
|
+
" switch k := key.(type) {",
|
|
33041
|
+
" case *rsa.PrivateKey:",
|
|
33042
|
+
' // Re-marshal to PKCS1 so DER matches "RSA PRIVATE KEY" header',
|
|
33043
|
+
" pkcs1DER := x509.MarshalPKCS1PrivateKey(k)",
|
|
33044
|
+
' return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcs1DER})',
|
|
33045
|
+
" case *ecdsa.PrivateKey:",
|
|
33046
|
+
" ecDER, ecErr := x509.MarshalECPrivateKey(k)",
|
|
33047
|
+
" if ecErr == nil {",
|
|
33048
|
+
' return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: ecDER})',
|
|
33049
|
+
" }",
|
|
33050
|
+
' return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})',
|
|
33051
|
+
" default:",
|
|
33052
|
+
' return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})',
|
|
33053
|
+
" }",
|
|
32934
33054
|
" }",
|
|
32935
|
-
"
|
|
32936
|
-
"
|
|
32937
|
-
|
|
33055
|
+
"",
|
|
33056
|
+
" // Try RSA PKCS1",
|
|
33057
|
+
" if _, err := x509.ParsePKCS1PrivateKey(keyDER); err == nil {",
|
|
33058
|
+
' return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDER})',
|
|
32938
33059
|
" }",
|
|
32939
|
-
"
|
|
32940
|
-
"
|
|
32941
|
-
|
|
33060
|
+
"",
|
|
33061
|
+
" // Try EC",
|
|
33062
|
+
" if _, err := x509.ParseECPrivateKey(keyDER); err == nil {",
|
|
33063
|
+
' return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})',
|
|
32942
33064
|
" }",
|
|
32943
|
-
"
|
|
32944
|
-
|
|
33065
|
+
"",
|
|
33066
|
+
" // Fallback \u2014 generic PRIVATE KEY header",
|
|
33067
|
+
' return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})',
|
|
32945
33068
|
"}",
|
|
32946
33069
|
"",
|
|
32947
33070
|
"// buildCombinedPEM creates a combined PEM file with cert + chain + key.",
|
|
32948
33071
|
"// Used when the target expects all components in a single file.",
|
|
33072
|
+
"// NOTE: This assumes CertificateBundle uses []byte fields (Option B).",
|
|
33073
|
+
"// If using string fields (Option A), decode from base64 first.",
|
|
32949
33074
|
"func buildCombinedPEM(bundle *domain.CertificateBundle) ([]byte, error) {",
|
|
32950
33075
|
" var pemContent strings.Builder",
|
|
32951
33076
|
"",
|
|
@@ -32963,12 +33088,8 @@ var TEMPLATES = {
|
|
|
32963
33088
|
" })))",
|
|
32964
33089
|
" }",
|
|
32965
33090
|
"",
|
|
32966
|
-
" // Private key (with
|
|
32967
|
-
"
|
|
32968
|
-
" pemContent.WriteString(string(pem.EncodeToMemory(&pem.Block{",
|
|
32969
|
-
" Type: keyType,",
|
|
32970
|
-
" Bytes: bundle.PrivateKey,",
|
|
32971
|
-
" })))",
|
|
33091
|
+
" // Private key (with PKCS8 re-marshaling)",
|
|
33092
|
+
" pemContent.WriteString(string(encodeKeyToPEM(bundle.PrivateKey)))",
|
|
32972
33093
|
"",
|
|
32973
33094
|
" return []byte(pemContent.String()), nil",
|
|
32974
33095
|
"}",
|
|
@@ -32996,12 +33117,9 @@ var TEMPLATES = {
|
|
|
32996
33117
|
"}",
|
|
32997
33118
|
"",
|
|
32998
33119
|
"// buildKeyPEM creates a PEM file with just the private key.",
|
|
33120
|
+
"// Re-marshals PKCS8 keys to native format for PEM header correctness.",
|
|
32999
33121
|
"func buildKeyPEM(bundle *domain.CertificateBundle) []byte {",
|
|
33000
|
-
"
|
|
33001
|
-
" return pem.EncodeToMemory(&pem.Block{",
|
|
33002
|
-
" Type: keyType,",
|
|
33003
|
-
" Bytes: bundle.PrivateKey,",
|
|
33004
|
-
" })",
|
|
33122
|
+
" return encodeKeyToPEM(bundle.PrivateKey)",
|
|
33005
33123
|
"}",
|
|
33006
33124
|
"",
|
|
33007
33125
|
"// backupFile creates a backup of an existing file before overwriting.",
|
|
@@ -33247,7 +33365,7 @@ func configureLogger() (*zap.Logger, error) {
|
|
|
33247
33365
|
},
|
|
33248
33366
|
"rest-client.go": {
|
|
33249
33367
|
name: "rest-client.go",
|
|
33250
|
-
description: "REST API client (handler) pattern for network appliance connectors (
|
|
33368
|
+
description: "REST API client (handler) pattern for network appliance connectors (firewalls, load balancers, ADCs). Uses go-resty with multi-auth support (API token, session, PKI).",
|
|
33251
33369
|
targetPath: "internal/app/<target>/handler.go",
|
|
33252
33370
|
content: [
|
|
33253
33371
|
"package <target>",
|
|
@@ -33384,7 +33502,7 @@ func configureLogger() (*zap.Logger, error) {
|
|
|
33384
33502
|
},
|
|
33385
33503
|
"rest-api-service-pattern.go": {
|
|
33386
33504
|
name: "rest-api-service-pattern.go",
|
|
33387
|
-
description: "3-service decomposition pattern for REST API connectors
|
|
33505
|
+
description: "3-service decomposition pattern for REST API connectors. Connection, Provisioning, and Discovery as separate services.",
|
|
33388
33506
|
targetPath: "internal/app/<target>/<target>.go",
|
|
33389
33507
|
content: [
|
|
33390
33508
|
"package <target>",
|
|
@@ -33557,14 +33675,17 @@ ${content}`;
|
|
|
33557
33675
|
function getMachineEndpoints(args) {
|
|
33558
33676
|
const webTemplate = TEMPLATES["machine-web.go"];
|
|
33559
33677
|
const testConnTemplate = TEMPLATES["test-connection.go"];
|
|
33678
|
+
const discoveryTypesTemplate = TEMPLATES["discovery-types.go"];
|
|
33560
33679
|
let webContent = webTemplate.content;
|
|
33561
33680
|
let testConnContent = testConnTemplate.content;
|
|
33681
|
+
let discoveryTypesContent = discoveryTypesTemplate.content;
|
|
33562
33682
|
if (args?.connectorName || args?.modulePath || args?.targetPackage) {
|
|
33563
33683
|
const cn = args?.connectorName || "<CONNECTOR_NAME>";
|
|
33564
33684
|
const mp = args?.modulePath || "<CONNECTOR_MODULE>";
|
|
33565
33685
|
const tp = args?.targetPackage || "<target>";
|
|
33566
33686
|
webContent = substituteTemplate(webContent, cn, mp, tp);
|
|
33567
33687
|
testConnContent = substituteTemplate(testConnContent, cn, mp, tp);
|
|
33688
|
+
discoveryTypesContent = substituteTemplate(discoveryTypesContent, cn, mp, tp);
|
|
33568
33689
|
}
|
|
33569
33690
|
return `# Machine Connector Endpoint Templates
|
|
33570
33691
|
|
|
@@ -33576,9 +33697,9 @@ A machine connector implements 5 endpoints (plus /healthz). All are POST endpoin
|
|
|
33576
33697
|
|---|---|---|
|
|
33577
33698
|
| POST /v1/testconnection | HandleTestConnection | Validate connectivity, detect target software |
|
|
33578
33699
|
| POST /v1/discovercertificates | HandleDiscoverCertificates | Find certificates, return with keystore/binding metadata |
|
|
33579
|
-
| POST /v1/installcertificatebundle | HandleInstallCertificateBundle |
|
|
33580
|
-
| POST /v1/configureinstallationendpoint | HandleConfigureInstallationEndpoint |
|
|
33581
|
-
| POST /v1/gettargetconfiguration | HandleGetTargetConfiguration | Return target info (stub is OK) |
|
|
33700
|
+
| POST /v1/installcertificatebundle | HandleInstallCertificateBundle | Install cert+chain+key on target |
|
|
33701
|
+
| POST /v1/configureinstallationendpoint | HandleConfigureInstallationEndpoint | Configure service binding for the installed cert |
|
|
33702
|
+
| POST /v1/gettargetconfiguration | HandleGetTargetConfiguration | Return target info for UI dropdowns (stub is OK) |
|
|
33582
33703
|
| GET /healthz | (inline) | Kubernetes liveness probe |
|
|
33583
33704
|
|
|
33584
33705
|
## WebhookService Interface (web.go)
|
|
@@ -33597,17 +33718,172 @@ This is a complete test connection handler with SSH validation. Customize step 4
|
|
|
33597
33718
|
${testConnContent}
|
|
33598
33719
|
\`\`\`
|
|
33599
33720
|
|
|
33721
|
+
## Discovery Response Types (discovery/types.go)
|
|
33722
|
+
|
|
33723
|
+
These types define the discovery request/response structures. The response format is critical \u2014 incorrect types cause silent failures.
|
|
33724
|
+
|
|
33725
|
+
\`\`\`go
|
|
33726
|
+
${discoveryTypesContent}
|
|
33727
|
+
\`\`\`
|
|
33728
|
+
|
|
33729
|
+
## Install Certificate Bundle Handler
|
|
33730
|
+
|
|
33731
|
+
CRITICAL: The response MUST wrap the updated keystore in \`{"keystore": {...}}\`.
|
|
33732
|
+
Returning the keystore directly (without the wrapper) causes: \`"missing json schema in plugin manifest for entity keystoreId"\`.
|
|
33733
|
+
|
|
33734
|
+
\`\`\`go
|
|
33735
|
+
// File: internal/app/<target>/install_certificate_bundle.go
|
|
33736
|
+
|
|
33737
|
+
// InstallCertificateBundleRequest contains the request for installing a certificate.
|
|
33738
|
+
type InstallCertificateBundleRequest struct {
|
|
33739
|
+
Connection *domain.Connection \`json:"connection"\`
|
|
33740
|
+
Keystore *domain.Keystore \`json:"keystore"\`
|
|
33741
|
+
CertificateBundle *domain.CertificateBundle \`json:"certificateBundle"\`
|
|
33742
|
+
}
|
|
33743
|
+
|
|
33744
|
+
// InstallCertificateBundleResponse wraps the updated keystore.
|
|
33745
|
+
// CRITICAL: Response MUST be {"keystore": {...}} \u2014 platform validates against manifest schema.
|
|
33746
|
+
type InstallCertificateBundleResponse struct {
|
|
33747
|
+
Keystore domain.Keystore \`json:"keystore"\`
|
|
33748
|
+
}
|
|
33749
|
+
|
|
33750
|
+
func (svc *WebhookServiceImpl) HandleInstallCertificateBundle(c echo.Context) error {
|
|
33751
|
+
zap.L().Info("installCertificateBundle workflow activity started")
|
|
33752
|
+
|
|
33753
|
+
// Step 1: Parse request
|
|
33754
|
+
req := InstallCertificateBundleRequest{}
|
|
33755
|
+
if err := c.Bind(&req); err != nil {
|
|
33756
|
+
zap.L().Error("install step 1 failed: invalid request", zap.Error(err))
|
|
33757
|
+
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
33758
|
+
}
|
|
33759
|
+
|
|
33760
|
+
// Step 2: Connect to target
|
|
33761
|
+
client := svc.ClientServices.NewClient(req.Connection)
|
|
33762
|
+
if err := svc.ClientServices.Connect(client); err != nil {
|
|
33763
|
+
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
33764
|
+
}
|
|
33765
|
+
defer svc.ClientServices.Close(client)
|
|
33766
|
+
|
|
33767
|
+
// Step 3: Install the certificate
|
|
33768
|
+
// CUSTOMIZE: Implement your target-specific installation logic here.
|
|
33769
|
+
// For SSH targets: write PEM files to disk, set permissions.
|
|
33770
|
+
// For REST API targets: upload via API (PKCS12, PEM, or target-specific format).
|
|
33771
|
+
//
|
|
33772
|
+
// Certificate bundle fields are base64-encoded DER (if using string fields):
|
|
33773
|
+
// certDER, _ := base64.StdEncoding.DecodeString(req.CertificateBundle.Certificate)
|
|
33774
|
+
// keyDER, _ := base64.StdEncoding.DecodeString(req.CertificateBundle.PrivateKey)
|
|
33775
|
+
// for _, entry := range req.CertificateBundle.CertificateChain {
|
|
33776
|
+
// chainDER, _ := base64.StdEncoding.DecodeString(entry)
|
|
33777
|
+
// }
|
|
33778
|
+
//
|
|
33779
|
+
// If using []byte fields, Go auto-decodes \u2014 fields already contain DER bytes.
|
|
33780
|
+
|
|
33781
|
+
updatedKeystore := *req.Keystore // Copy and update as needed
|
|
33782
|
+
|
|
33783
|
+
zap.L().Info("installCertificateBundle completed",
|
|
33784
|
+
zap.Any("keystore", updatedKeystore),
|
|
33785
|
+
)
|
|
33786
|
+
|
|
33787
|
+
// CRITICAL: Wrap in {"keystore": {...}} \u2014 do NOT return the keystore directly
|
|
33788
|
+
return c.JSON(http.StatusOK, InstallCertificateBundleResponse{Keystore: updatedKeystore})
|
|
33789
|
+
}
|
|
33790
|
+
\`\`\`
|
|
33791
|
+
|
|
33792
|
+
## Configure Installation Endpoint Handler
|
|
33793
|
+
|
|
33794
|
+
CRITICAL: The response MUST wrap the binding in \`{"binding": {...}}\`.
|
|
33795
|
+
|
|
33796
|
+
\`\`\`go
|
|
33797
|
+
// File: internal/app/<target>/configure_installation_endpoint.go
|
|
33798
|
+
|
|
33799
|
+
// ConfigureInstallationEndpointRequest contains the request for configuring the binding.
|
|
33800
|
+
type ConfigureInstallationEndpointRequest struct {
|
|
33801
|
+
Connection *domain.Connection \`json:"connection"\`
|
|
33802
|
+
Keystore *domain.Keystore \`json:"keystore"\`
|
|
33803
|
+
Binding *domain.Binding \`json:"binding"\`
|
|
33804
|
+
}
|
|
33805
|
+
|
|
33806
|
+
// ConfigureInstallationEndpointResponse wraps the binding.
|
|
33807
|
+
// CRITICAL: Response MUST be {"binding": {...}} \u2014 platform validates against manifest schema.
|
|
33808
|
+
type ConfigureInstallationEndpointResponse struct {
|
|
33809
|
+
Binding domain.Binding \`json:"binding"\`
|
|
33810
|
+
}
|
|
33811
|
+
|
|
33812
|
+
func (svc *WebhookServiceImpl) HandleConfigureInstallationEndpoint(c echo.Context) error {
|
|
33813
|
+
zap.L().Info("configureInstallationEndpoint workflow activity started")
|
|
33814
|
+
|
|
33815
|
+
// Step 1: Parse request
|
|
33816
|
+
req := ConfigureInstallationEndpointRequest{}
|
|
33817
|
+
if err := c.Bind(&req); err != nil {
|
|
33818
|
+
zap.L().Error("configure step 1 failed: invalid request", zap.Error(err))
|
|
33819
|
+
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
33820
|
+
}
|
|
33821
|
+
|
|
33822
|
+
// Step 2: Connect to target
|
|
33823
|
+
client := svc.ClientServices.NewClient(req.Connection)
|
|
33824
|
+
if err := svc.ClientServices.Connect(client); err != nil {
|
|
33825
|
+
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
33826
|
+
}
|
|
33827
|
+
defer svc.ClientServices.Close(client)
|
|
33828
|
+
|
|
33829
|
+
// Step 3: Configure the binding
|
|
33830
|
+
// CUSTOMIZE: Implement your target-specific binding logic here.
|
|
33831
|
+
// For SSH targets: restart/reload the service (systemctl restart, service reload, etc.).
|
|
33832
|
+
// For REST API targets: update the binding via API (assign cert to VIP, profile, etc.).
|
|
33833
|
+
|
|
33834
|
+
zap.L().Info("configureInstallationEndpoint completed",
|
|
33835
|
+
zap.Any("binding", req.Binding),
|
|
33836
|
+
)
|
|
33837
|
+
|
|
33838
|
+
// CRITICAL: Wrap in {"binding": {...}} \u2014 do NOT return the binding directly
|
|
33839
|
+
return c.JSON(http.StatusOK, ConfigureInstallationEndpointResponse{Binding: *req.Binding})
|
|
33840
|
+
}
|
|
33841
|
+
\`\`\`
|
|
33842
|
+
|
|
33843
|
+
## Get Target Configuration Handler
|
|
33844
|
+
|
|
33845
|
+
Returns target metadata for UI dropdowns. Can be a stub initially.
|
|
33846
|
+
|
|
33847
|
+
\`\`\`go
|
|
33848
|
+
// File: internal/app/<target>/get_target_configuration.go
|
|
33849
|
+
|
|
33850
|
+
type GetTargetConfigurationRequest struct {
|
|
33851
|
+
Connection *domain.Connection \`json:"connection"\`
|
|
33852
|
+
}
|
|
33853
|
+
|
|
33854
|
+
func (svc *WebhookServiceImpl) HandleGetTargetConfiguration(c echo.Context) error {
|
|
33855
|
+
zap.L().Info("getTargetConfiguration workflow activity started")
|
|
33856
|
+
|
|
33857
|
+
req := GetTargetConfigurationRequest{}
|
|
33858
|
+
if err := c.Bind(&req); err != nil {
|
|
33859
|
+
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
33860
|
+
}
|
|
33861
|
+
|
|
33862
|
+
// CUSTOMIZE: Return target configuration for UI dropdowns.
|
|
33863
|
+
// Fields returned here populate x-targetConfigurationRef dropdowns in the manifest.
|
|
33864
|
+
// Return an empty object if no dynamic configuration is needed.
|
|
33865
|
+
return c.JSON(http.StatusOK, map[string]interface{}{})
|
|
33866
|
+
}
|
|
33867
|
+
\`\`\`
|
|
33868
|
+
|
|
33600
33869
|
## Handler Pattern (Use for ALL Endpoints)
|
|
33601
33870
|
|
|
33602
33871
|
Every handler follows these steps:
|
|
33603
33872
|
1. Parse and validate the request
|
|
33604
33873
|
2. Establish connection (with defer Close)
|
|
33605
33874
|
3. Do the actual work
|
|
33606
|
-
4. Return response
|
|
33875
|
+
4. Return response as JSON
|
|
33876
|
+
|
|
33877
|
+
### Critical Response Rules
|
|
33878
|
+
- **testConnection**: Return \`c.JSON(http.StatusOK, TestConnectionResponse{Result: true})\`
|
|
33879
|
+
- **installCertificateBundle**: Return \`c.JSON(http.StatusOK, InstallCertificateBundleResponse{Keystore: ...})\` \u2014 MUST wrap in \`{"keystore": ...}\`
|
|
33880
|
+
- **configureInstallationEndpoint**: Return \`c.JSON(http.StatusOK, ConfigureInstallationEndpointResponse{Binding: ...})\` \u2014 MUST wrap in \`{"binding": ...}\`
|
|
33881
|
+
- **discoverCertificates**: Return \`c.JSON(http.StatusOK, DiscoverCertificatesResponse{Messages: ...})\` \u2014 MUST use \`"messages"\` key
|
|
33882
|
+
- **ALL responses must be JSON** \u2014 the vSatellite unmarshals every webhook response as JSON. Returning plain text (\`c.String(200, "OK")\`) causes: \`"invalid character 'O' looking for beginning of value"\`
|
|
33883
|
+
- Return HTTP 400 for ALL errors (not 500)
|
|
33607
33884
|
|
|
33608
|
-
|
|
33885
|
+
### Other Rules
|
|
33609
33886
|
- Always defer Close() immediately after Connect()
|
|
33610
|
-
- Return HTTP 400 for ALL errors (not 500)
|
|
33611
33887
|
- Log every step with structured fields
|
|
33612
33888
|
- Each endpoint is stateless \u2014 connect, do work, disconnect
|
|
33613
33889
|
`;
|
|
@@ -33615,7 +33891,7 @@ Key rules:
|
|
|
33615
33891
|
function getMachineBestPractices() {
|
|
33616
33892
|
return `# Machine Connector Best Practices
|
|
33617
33893
|
|
|
33618
|
-
These best practices are distilled from building multiple machine connectors
|
|
33894
|
+
These best practices are distilled from building multiple machine connectors across SSH-based and REST API-based targets. They cover what worked, what failed, and what to avoid.
|
|
33619
33895
|
|
|
33620
33896
|
${LESSONS_LEARNED}
|
|
33621
33897
|
|
|
@@ -33641,12 +33917,18 @@ ${LESSONS_LEARNED}
|
|
|
33641
33917
|
18. **Retired certificates in Venafi silently block discovery** \u2014 query certificateStatus for RETIRED records when MI count is low
|
|
33642
33918
|
19. **Null-safe JSON arrays** \u2014 initialize slices to \`[]string{}\` not nil, as \`null\` in JSON causes silent discovery failures
|
|
33643
33919
|
20. **discoveryTypes label MUST use "title"** \u2014 \`x-labelLocalizationKey\` does NOT work for array discovery fields (shows raw key text). Use \`"title": "Certificate Types"\` on the property and \`"title"\` on each oneOf item. Boolean discovery fields CAN use \`x-labelLocalizationKey\` with the \`"discovery.xxx"\` pattern
|
|
33644
|
-
21. **Discovery type checkboxes should match the target platform's certificate categories** \u2014 ask "how would an admin describe their certificates?" (
|
|
33920
|
+
21. **Discovery type checkboxes should match the target platform's certificate categories** \u2014 ask "how would an admin describe their certificates?" (network appliance: by service type; API platform: by usage role). Don't use generic/internal labels
|
|
33645
33921
|
22. **x-labelLocalizationKey must be two-level dot paths** \u2014 \`"fieldName.label"\` works, but \`"section.fieldName.label"\` (three levels) does NOT resolve and shows raw key text in the UI. Use flat prefixes like \`"dpDomain.label"\` instead of \`"keystore.domain.label"\`
|
|
33646
33922
|
23. **certificateBundle.certificateChain is an ARRAY** \u2014 Venafi Cloud sends it as \`["base64cert1", "base64cert2"]\`, not a single string. Manifest must declare \`{ "type": "array", "items": { "contentEncoding": "base64", "type": "string" } }\`. Go struct uses \`[][]byte\` (auto base64 decode) or \`[]string\` (manual decode)
|
|
33647
33923
|
24. **Certificate chain ordering is undocumented** \u2014 Venafi Cloud sends the chain array but the ordering guarantee is not documented. Pass through in received order and note this assumption. Targets with strict ordering requirements may need issuer/subject-based re-sorting
|
|
33648
33924
|
25. **REST API discovery: convert base64 DER to PEM** \u2014 when a target API returns certs as base64-encoded DER, you MUST convert to PEM for the discovery response (\`base64.Decode \u2192 pem.EncodeToMemory\`). Validate with \`strings.HasPrefix(cert, "-----BEGIN CERTIFICATE-----")\`
|
|
33649
33925
|
26. **Default binding must be assigned BEFORE discovery type filtering** \u2014 if "unbound" (or any fallback binding) is assigned after the filter runs, unbound certs are silently dropped. Always: find bindings \u2192 assign default if empty \u2192 then filter
|
|
33926
|
+
27. **Discovery messages MUST include an \`installations\` array** \u2014 each DiscoveredCertificate must have at least one entry with hostname, ipAddress, and port. Without this field, the platform silently discards the certificate \u2014 discovery shows "complete" but no certs appear in the UI. This is the most common cause of "discovery works but nothing shows up"
|
|
33927
|
+
28. **Connection reset after cert import = SUCCESS** \u2014 some targets restart their web gateway or management interface after a certificate is installed, causing connection resets, timeouts, or HTTP errors. These MUST be caught and treated as success, not failure. Add a short retry/delay if you need to verify the install afterward
|
|
33928
|
+
29. **Idempotent install detection** \u2014 HTTP 409 or response body containing "already exists" / "identical" during cert import should be treated as success, not error. The certificate was already installed (e.g., from a previous attempt that reported a connection error)
|
|
33929
|
+
30. **Error messages must attribute the component** \u2014 prefix all errors so users can identify which system caused the failure: \`"Target API error: ..."\` (target returned an error), \`"Target connection error: ..."\` (cannot reach target), \`"Connector error: ..."\` (invalid request or internal processing error). This saves significant support/debugging time
|
|
33930
|
+
31. **Binding is optional in the manifest** \u2014 if your target has no binding concept (single-cert targets), you can omit binding from the domainSchema. However, if binding was ever defined and machines exist with binding data, you MUST keep at least \`{"properties": {}, "type": "object"}\` or the Venafi UI will crash when editing those machines. \`x-primaryKey\` is optional on binding when it has no properties, but always required on keystore
|
|
33931
|
+
32. **Dropdowns use \`oneOf\` with \`const\`, not \`enum\`** \u2014 the correct manifest pattern for dropdown fields is \`"oneOf": [{"const": "value", "x-labelLocalizationKey": "field.value"}]\`. Using \`enum\` alone does not render labels in the UI
|
|
33650
33932
|
`;
|
|
33651
33933
|
}
|
|
33652
33934
|
function getRESTClientPattern(args) {
|
|
@@ -33666,7 +33948,7 @@ function getRESTClientPattern(args) {
|
|
|
33666
33948
|
}
|
|
33667
33949
|
return `# REST API Client Pattern for Machine Connectors
|
|
33668
33950
|
|
|
33669
|
-
The REST API client pattern is for network appliance connectors (
|
|
33951
|
+
The REST API client pattern is for network appliance connectors (firewalls, load balancers, ADCs, management platforms) that communicate via HTTPS REST API instead of SSH.
|
|
33670
33952
|
|
|
33671
33953
|
## When to Use This Pattern
|
|
33672
33954
|
- Target is a network appliance (firewall, load balancer, ADC)
|
|
@@ -33681,7 +33963,7 @@ The REST API client pattern is for network appliance connectors (FortiGate, F5,
|
|
|
33681
33963
|
- No sudo, no file system ops \u2014 everything is API calls
|
|
33682
33964
|
- Each request creates a fresh handler (stateless per endpoint call)
|
|
33683
33965
|
|
|
33684
|
-
## Architecture: 3-Service Decomposition
|
|
33966
|
+
## Architecture: 3-Service Decomposition
|
|
33685
33967
|
|
|
33686
33968
|
Instead of a single \`ClientServices\` interface, REST API connectors use three services:
|
|
33687
33969
|
- **ConnectionService** \u2014 handles \`testConnection\`
|
|
@@ -33711,7 +33993,7 @@ ${appContent}
|
|
|
33711
33993
|
## Network Appliance Patterns
|
|
33712
33994
|
|
|
33713
33995
|
### Certificate Cannot Be Updated In-Place
|
|
33714
|
-
Many appliances
|
|
33996
|
+
Many appliances cannot update a certificate's content in-place. The replacement pattern is:
|
|
33715
33997
|
1. Upload new cert with a **different name** (e.g., \`name_YYMonDD_serial\`)
|
|
33716
33998
|
2. Update all bindings to reference the new name
|
|
33717
33999
|
3. Delete the old certificate (only if no bindings remain)
|
|
@@ -33741,10 +34023,8 @@ Changing the management interface certificate drops the active HTTPS session.
|
|
|
33741
34023
|
Catch the connection error and treat it as success.
|
|
33742
34024
|
|
|
33743
34025
|
## Reference Connectors
|
|
33744
|
-
- **PAN Panorama** \u2014 cleanest architecture, 3-service decomposition, config locking
|
|
33745
|
-
- **Citrix ADC** \u2014 NITRO REST API, partition switching, SNI handling, comprehensive tests
|
|
33746
|
-
- **F5** \u2014 simpler flat structure, go-bigip SDK, file upload pattern
|
|
33747
34026
|
- **VMware AVI** (github.com/Venafi/vmware-avi-connector) \u2014 public REST API reference
|
|
34027
|
+
- Reference connectors using 3-service decomposition, partition switching, and SNI handling are available internally
|
|
33748
34028
|
`;
|
|
33749
34029
|
}
|
|
33750
34030
|
function getSSHClientPattern(args) {
|
|
@@ -33857,7 +34137,7 @@ This means: no shared state between calls. Each call connects, does work, discon
|
|
|
33857
34137
|
- \`get_machine_manifest\` \u2014 Get the machine connector manifest.json template with all sections explained
|
|
33858
34138
|
- \`get_machine_domain_types\` \u2014 Get Go domain type templates (Connection, Keystore, Binding, CertificateBundle, Client)
|
|
33859
34139
|
- \`get_machine_endpoints\` \u2014 Get handler/service interface templates for all 5 machine endpoints
|
|
33860
|
-
- \`get_machine_best_practices\` \u2014 Get lessons learned and best practices from building
|
|
34140
|
+
- \`get_machine_best_practices\` \u2014 Get lessons learned and best practices from building reference connectors
|
|
33861
34141
|
- \`get_ssh_client_pattern\` \u2014 Get the SSH client abstraction code with sudo, file I/O, and command execution
|
|
33862
34142
|
|
|
33863
34143
|
## What I Need From You
|
|
@@ -33889,7 +34169,7 @@ server.resource("machine-blueprint", "venafi://connector-machine/blueprint", {
|
|
|
33889
34169
|
]
|
|
33890
34170
|
}));
|
|
33891
34171
|
server.resource("lessons-learned", "venafi://connector-machine/lessons-learned", {
|
|
33892
|
-
description: "What worked, what failed, and mistakes to avoid \u2014 from building
|
|
34172
|
+
description: "What worked, what failed, and mistakes to avoid \u2014 from building multiple machine connectors. Covers SSH and REST API patterns, discovery challenges, certificate format issues, and more.",
|
|
33893
34173
|
mimeType: "text/markdown"
|
|
33894
34174
|
}, async () => ({
|
|
33895
34175
|
contents: [
|
|
@@ -33944,7 +34224,7 @@ server.tool("get_machine_endpoints", "Return handler and service interface templ
|
|
|
33944
34224
|
}
|
|
33945
34225
|
]
|
|
33946
34226
|
}));
|
|
33947
|
-
server.tool("get_machine_best_practices", "Return lessons learned and best practices from building
|
|
34227
|
+
server.tool("get_machine_best_practices", "Return lessons learned and best practices from building multiple machine connectors (SSH and REST API). Covers: what worked (Avi reference, logging, interfaces, DI), mistakes (DER vs PEM, key types, pagination, error handling, response format), and things that need improvement (unit tests, integration tests, error context, timeouts).", {}, async () => ({
|
|
33948
34228
|
content: [
|
|
33949
34229
|
{
|
|
33950
34230
|
type: "text",
|
|
@@ -33964,7 +34244,7 @@ server.tool("get_ssh_client_pattern", "Return the SSH client abstraction code fo
|
|
|
33964
34244
|
}
|
|
33965
34245
|
]
|
|
33966
34246
|
}));
|
|
33967
|
-
server.tool("get_rest_client_pattern", "Return the REST API client abstraction code for network appliance machine connectors (
|
|
34247
|
+
server.tool("get_rest_client_pattern", "Return the REST API client abstraction code for network appliance machine connectors (firewalls, load balancers, ADCs, management platforms). Includes the Handler interface with multi-auth support (API token, username/password, PKI client cert), 3-service decomposition (ConnectionService, ProvisioningService, DiscoveryService), and uber/fx DI wiring. Use this instead of get_ssh_client_pattern when the target is a network appliance with a REST API.", {
|
|
33968
34248
|
connectorName: external_exports3.string().optional().describe("Connector name (e.g., 'fortigate-connector'). Replaces <CONNECTOR_NAME>."),
|
|
33969
34249
|
modulePath: external_exports3.string().optional().describe("Go module path (e.g., 'github.com/venafi/fortigate-connector'). Replaces <CONNECTOR_MODULE>."),
|
|
33970
34250
|
targetPackage: external_exports3.string().optional().describe("Target Go package name (e.g., 'fortigate'). Replaces <target>.")
|
|
@@ -33977,7 +34257,7 @@ server.tool("get_rest_client_pattern", "Return the REST API client abstraction c
|
|
|
33977
34257
|
]
|
|
33978
34258
|
}));
|
|
33979
34259
|
server.prompt("new_machine_connector", "Start building a new Venafi machine connector. Provides all context, tools, and instructions needed to begin building a machine connector for a specific target software.", {
|
|
33980
|
-
targetSoftware: external_exports3.string().describe("The target software (e.g., 'Apache HTTP Server', 'Nginx', 'HAProxy', '
|
|
34260
|
+
targetSoftware: external_exports3.string().describe("The target software (e.g., 'Apache HTTP Server', 'Nginx', 'HAProxy', 'network appliance')"),
|
|
33981
34261
|
connectionMethod: external_exports3.string().optional().default("ssh").describe("Connection method: 'ssh' or 'api'")
|
|
33982
34262
|
}, async (args) => ({
|
|
33983
34263
|
messages: [
|
package/package.json
CHANGED