Integration Best Practices
Lessons learned from building CA connectors, machine connectors, and Adaptable drivers across dozens of platforms.
General Principles
Start with a Reference Implementation
CA Connectors: Study the DigiCert ONE connector for complete examples.
Machine Connectors: Clone the VMware Avi connector as your starting point. It includes:
- Correct project structure for vSatellite deployment
- Working Makefile with all required build targets
- Proper Dockerfile pattern (distroless, nonroot)
- Payload encryption middleware
- Correct manifest.json structure
- Proven dependency versions
Adaptable Drivers: Review existing PowerShell drivers in the marketplace (search for "Adaptable" tag).
Don't start from scratch — reference implementations save weeks of trial-and-error.
Error Handling
Extract Error Details from Responses
APIs return errors in different formats. Implement flexible parsing:
Go (REST API):
func parseAPIError(resp *resty.Response) error {
var errResp struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
Message string `json:"message"`
}
json.Unmarshal(resp.Body(), &errResp)
// Try multiple formats
if errResp.Error.Message != "" {
return fmt.Errorf("%s: %s", errResp.Error.Code, errResp.Error.Message)
}
if len(errResp.Errors) > 0 {
return fmt.Errorf("%s", errResp.Errors[0].Message)
}
if errResp.Message != "" {
return fmt.Errorf("%s", errResp.Message)
}
// Fallback to raw body
return fmt.Errorf("API error: %s", string(resp.Body()))
}
PowerShell (Adaptable Driver):
try {
Invoke-RestMethod -Uri $uri
}
catch {
$stream = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($stream)
$errorBody = $reader.ReadToEnd() | ConvertFrom-Json
# Try multiple error fields
$message = $errorBody.error ?? $errorBody.message ?? $errorBody.detail ?? "Unknown error"
throw "[error] API call failed: $message"
}
Return Proper HTTP Status Codes
Connectors (Go): Return HTTP 400 for all errors, HTTP 200 for success. The Venafi framework expects this convention.
Adaptable Drivers: Throw exceptions with descriptive messages. Prefix with [error] for easy log filtering.
Logging
Use Structured Logging
Go (zap):
logger.Debug("Installing certificate",
zap.String("hostname", hostname),
zap.String("certId", certId),
zap.String("operation", "install"),
)
PowerShell:
Write-Host "[info] Installing certificate: certId=$certId, hostname=$hostname"
Write-Host "[error] Installation failed: $($_.Exception.Message)"
Log at Debug Level in Production
For connectors running on vSatellite, container logs are the only debugging interface. Set log level to Debug:
loggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
Log every step: authentication, API calls, response parsing, file operations.
What to Log
✅ Do log:
- Every API call (URL, method, status code)
- Authentication attempts (success/failure)
- Certificate operations (install, extract, discover)
- Parsed response data (IDs, names, counts)
- Error details (codes, messages, stack traces)
❌ Don't log:
- Passwords or API keys
- Private key material
- Full certificate PEMs (log thumbprints instead)
- Sensitive user data
Authentication
OAuth2 Client Credentials (Recommended)
Best for: Modern REST APIs with service account support.
Implementation:
- Request token per function call (don't cache)
- Store client ID and secret in secure fields
- Handle token errors gracefully
Example (PowerShell):
function Get-OAuthToken {
param($ClientId, $ClientSecret, $TokenUrl)
$body = @{
grant_type = "client_credentials"
client_id = $ClientId
client_secret = $ClientSecret
}
$response = Invoke-RestMethod -Uri $TokenUrl -Method Post -Body $body
return $response.access_token
}
API Key Authentication
Best for: Platforms with header-based auth.
Implementation:
- Store key in Passwd/VarPass field
- Include in Authorization or custom header
- Validate key format in Test-Settings
Avoid Username/Password When Possible
OAuth2 or API keys are more secure and easier to rotate.
Product Authentication Requirements
API key authentication (tppl-api-key header) is not supported on the NGTS platform. If your integration targets NGTS, you must implement the OAuth 2.0 Client Credentials flow.
| Product | Authentication Method | Details |
|---|---|---|
| Certificate Manager SaaS | API key (tppl-api-key header) | Key passed as custom header on every request |
| NGTS | OAuth 2.0 Client Credentials | Service account Client ID + Client Secret exchanged for Bearer token |
| Certificate Manager Self-Hosted | OAuth 2.0 or API Key | Depends on TPP configuration |
If your integration supports multiple products, implement both authentication methods and select the appropriate one based on the product configured by the user. See Product Differences for a full comparison.
The application and team resources do not exist in NGTS. These fields are accepted on requests for backward compatibility but are ignored. Do not build integration logic that depends on Applications or Teams when targeting NGTS.
Certificate Handling
Certificate Format Conversion
Venafi expects: Base64-encoded DER bytes (not PEM strings).
Conversion pattern:
// Parse PEM
block, _ := pem.Decode([]byte(pemString))
if block == nil {
return errors.New("invalid PEM")
}
// Return base64 DER
certBase64 := base64.StdEncoding.EncodeToString(block.Bytes)
Handle both PEM and DER input: Some APIs return PEM, others return base64 DER. Try PEM first, fall back to DER.
CSR Handling
Send CSR with PEM headers: Most CA APIs expect full PEM including -----BEGIN CERTIFICATE REQUEST-----.
Parse CSR to extract attributes: Even if the CSR contains subject/SANs, many CAs require these as separate fields.
csr, _ := x509.ParseCertificateRequest(csrBytes)
subject := map[string]string{
"common_name": csr.Subject.CommonName,
"organization": csr.Subject.Organization[0],
}
sans := csr.DNSNames
DER vs PEM Pitfalls
Common mistakes:
- Returning PEM when Venafi expects DER
- Stripping PEM headers when CA expects them
- Not validating certificate parsing before returning
Test both directions: Create test cases for PEM→DER and DER→PEM conversions.
Discovery and Pagination
Always Implement Pagination
Production environments may have hundreds or thousands of certificates.
Pattern (Go):
allCerts := []Certificate{}
offset := 0
limit := 100
for {
resp := client.ListCertificates(offset, limit)
allCerts = append(allCerts, resp.Items...)
if len(resp.Items) < limit {
break // Last page
}
offset += limit
}
Pattern (PowerShell):
$allCerts = @()
$offset = 0
$limit = 100
do {
$response = Invoke-Api -Endpoint "/certs?offset=$offset&limit=$limit"
$allCerts += $response.items
$offset += $limit
} while ($response.items.Count -eq $limit)
Handle Parse Failures Gracefully
One bad certificate shouldn't abort discovery of all others.
for _, item := range apiResponse.Items {
cert, err := parseCertificate(item.PEM)
if err != nil {
logger.Warn("Failed to parse certificate", zap.String("id", item.ID), zap.Error(err))
continue // Skip this cert
}
results = append(results, cert)
}
Sanitize Identifiers
Product-specific IDs may contain special characters.
sanitized := regexp.MustCompile(`[^a-zA-Z0-9-]`).ReplaceAllString(name, "-")
sanitized = strings.Trim(sanitized, "-")
Testing Strategies
Unit Testing
Test service interfaces in isolation:
func TestInstallCertificate(t *testing.T) {
mockClient := &MockAPIClient{
InstallFunc: func(cert Certificate) (string, error) {
return "cert-123", nil
},
}
service := NewService(mockClient)
result, err := service.InstallCertificate(testCert)
assert.NoError(t, err)
assert.Equal(t, "cert-123", result.AssetID)
}
Integration Testing
For Connectors: Use VenProxy simulation utility to test against local endpoints.
For Adaptable Drivers: Test directly on the Certificate Manager Self-Hosted server with Test-Settings, Discovery jobs, and provisioning.
End-to-End Testing
Test the full lifecycle:
- Test-Settings (connectivity)
- Discovery (enumeration)
- Install (provisioning)
- Extract (validation)
- Renewal (install new + delete old)
- Revocation (if applicable)
Security Considerations
Never Log Credentials
// ❌ BAD
logger.Debug("Auth successful", zap.String("password", password))
// ✅ GOOD
logger.Debug("Auth successful", zap.String("username", username))
Validate All Input
func validateHostname(hostname string) error {
if hostname == "" {
return errors.New("hostname is required")
}
if strings.Contains(hostname, "..") {
return errors.New("invalid hostname")
}
return nil
}
Use TLS for All Connections
Only allow insecure connections via explicit opt-in flag (Option1, etc.).
Handle Self-Signed Certificates Carefully
Provide option to bypass certificate validation, but default to validation enabled.
Performance Optimization
Connection Pooling
Reuse HTTP clients instead of creating new ones per request:
var httpClient = &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 90 * time.Second,
},
}
Rate Limiting
Implement retry with backoff for HTTP 429 responses:
client := resty.New().
SetRetryCount(3).
SetRetryWaitTime(1 * time.Second).
SetRetryMaxWaitTime(30 * time.Second).
AddRetryCondition(func(r *resty.Response, err error) bool {
return r.StatusCode() == 429 // Rate limit
})
Batch Operations When Possible
Group API calls to reduce round-trips:
// ❌ BAD: N+1 queries
for _, certId := range certIds {
cert := apiClient.GetCertificate(certId)
}
// ✅ GOOD: Batch fetch
certs := apiClient.GetCertificatesBatch(certIds)
Common Pitfalls
CA Connectors
- ❌ Returning PEM instead of base64 DER for certificate fields
- ❌ Stripping PEM headers from CSR
- ❌ Using flat attribute structure instead of nested (e.g.,
attributes.subject.common_name) - ❌ Not implementing retry on HTTP 429 rate limits
- ❌ Hardcoding auth header name instead of making it configurable
Machine Connectors
- ❌ Not implementing pagination for discovery
- ❌ Failing entire batch when one certificate fails to parse
- ❌ Not cleaning up temp files in finally blocks
- ❌ Caching connections across handler calls (each call is independent)
- ❌ Not validating AssetName before using it in Extract-Certificate
Adaptable Drivers
- ❌ Missing stub implementations for unused functions (the platform validates all 10)
- ❌ Not re-saving policy after script changes (script won't be approved)
- ❌ Making old certificate deletion fatal during renewal
- ❌ Not sanitizing identifiers from target platforms
- ❌ Forgetting to set AssetName after successful installation
Deployment Checklist
For Go-Based Connectors
- Dockerfile uses distroless base image
- Container runs as nonroot user
- Makefile includes all required targets (build, test, docker-build, docker-push)
- Manifest.json validates against schema
- Payload encryption middleware is implemented
- All dependencies are pinned to specific versions
- Health check endpoint responds correctly
- Container logs are structured and debug-level
For PowerShell Adaptable Drivers
- All 10 required functions exist (even if stubs)
- Field definitions block is correctly formatted
- Test-Settings validates all required fields
- AssetName is set correctly after Install-Certificate
- Discovery implements pagination
- Temp files are cleaned up in finally blocks
- Script is approved in TPP WebAdmin after any changes
- Error messages are descriptive and prefixed with [error]
Next Steps
Apply these practices when building:
Review your integration against this checklist before deploying to production.