Skip to content

[TT-17295] feat: support embedded PEM certificate content in Gateway#8256

Merged
mativm02 merged 9 commits into
masterfrom
feat/TT-17295/embedded-pem-certs
Jun 12, 2026
Merged

[TT-17295] feat: support embedded PEM certificate content in Gateway#8256
mativm02 merged 9 commits into
masterfrom
feat/TT-17295/embedded-pem-certs

Conversation

@mativm02

@mativm02 mativm02 commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a third certificate-source mode to CertificateManager.List() / CertPool(): literal PEM content (in addition to the existing SHA256 cert ID and filesystem path modes).
  • Backward-compatible: existing cert-ID and file-path flows are unchanged and all pre-existing tests continue to pass.
  • Unblocks future KV / secret-store substitution (Vault, Kubernetes Secrets) at API-load time without persisting private material in Redis.

Why

Per the ticket, customers want to embed a PEM-encoded certificate directly into their API definition (or have it substituted in by a secret-management layer) rather than uploading it to the Tyk certificate store via the management plane. Improves operational security, simplifies rotation, and is a prerequisite for the KV/secret-store substitution work tracked separately.

What changes

Production:

  • tyk/certs/manager.go — new exported helper IsPEMContent(string) bool, new private constant embeddedPEMCacheKeyPrefix = "embedded-pem-", and an embedded-PEM branch at the top of List(). Also adds a clarifying comment on ListPublicKeys() documenting the deliberate omission for pinned public keys (already accepts content per ticket).
  • tyk/gateway/cert_usage_tracker.goextractCertificatesFromSpec now skips inline PEM values via certs.IsPEMContent. The MDCB sync map exists to drive cert-sync targets; embedded PEMs are inline content and would otherwise bloat the map with multi-KB string keys.

Tests:

  • tyk/certs/manager_test.goTestList_EmbeddedPEM with 10 subtests (single cert, combined cert+key, mixed batch with file/storage-ID/PEM, leading whitespace, CRLF line endings, malformed body, truncated, content-hash caching, multi-cert chain, CertPool with embedded CA).
  • tyk/gateway/cert_embedded_pem_test.go (new) — 5 end-to-end TLS-handshake tests, one per in-scope field (client_certificates, upstream_certificates, certificates, global SSLCertificates, ControlAPI client-CA pool), plus a 3-source backward-compatibility table test exercising file path, cert ID, and embedded PEM through the same code path.
  • tyk/gateway/cert_usage_tracker_test.goTestExtractCertificatesFromSpec_SkipsEmbeddedPEM.

In-scope cert fields

All route through CertificateManager.List() / CertPool(), so the single change in manager.go lights all of them up transparently:

Field Loading site
client_certificates (mTLS allowlist) gateway/mw_certificate_check.go:111
certificates (per-API domain server cert) gateway/cert.go:535
upstream_certificates (per-host map values) gateway/cert.go:141
Global HttpServerOptions.SSLCertificates gateway/cert.go:424
Security.Certificates.ControlAPI (CA pool) gateway/cert.go:472CertPool()

Out of scope (follow-up tickets)

  • pinned_public_keys — already accepts content (fingerprints) per ticket; no change needed.
  • KV/secret-store reference resolution (vault://, secret-ref://) — pre-resolution happens before values reach CertificateManager.
  • Encryption-at-rest for embedded PEM in API definitions.
  • internal/httpclient/factory.go's MTLS.CAFile (separate internal-services config path, not in ticket's field list).

Safety notes

  • Detection ambiguity: IsPEMContent matches "-----BEGIN ". SHA256 hex IDs are hex-only (no dashes) and POSIX filesystem paths in practice never contain that sentinel — the three source modes remain unambiguous.
  • Private-key logging: the new branch deliberately does NOT log the PEM body on parse failure (only the error). The legacy fallback's Debug("Failed certificate: ", string(rawCert)) log is unreachable for embedded-PEM input because every embedded-PEM path terminates the loop iteration with continue.
  • Cache key collisions: the embedded-pem- prefix contains a - and is unlikely to be addressable as an org-id + cert-id concatenation, eliminating the theoretical operator-controlled cache shadow.
  • Concurrency: the underlying cache Set/Get is mutex-guarded; two parallel List() calls for the same embedded PEM at most duplicate one parse and converge on the same cached entry.

Test plan

  • go test ./tyk/certs/... — unit tests pass (including new TestList_EmbeddedPEM).
  • go test ./tyk/gateway/ -run 'Cert|TLS|Mutual|Embedded|Pinned|ClientCertif|UpstreamCert|ExtractCertificatesFromSpec' — all cert/TLS-adjacent gateway tests pass.
  • Pre-existing TestCertificateStorage (file + Redis paths) and TestUpstreamMutualTLS / TestAPIMutualTLS / TestGatewayTLS / TestAPICertificate / TestGatewayControlAPIMutualTLS / TestCertificateHandlerTLS continue to pass (backward-compat regression check).
  • go vet ./tyk/certs/... ./tyk/gateway/... clean.
  • go build ./tyk/... clean.
  • CI green.

Related

Inspired by TT-15079 (KV/secret-store substitution for certificate references). This PR implements the prerequisite Gateway-side change; the substitution layer is a follow-up.

Ticket Details

TT-17295
Status In Dev
Summary Support loading of certificates in gateway with certificate content

Generated at: 2026-05-29 13:16:32

Add a third certificate-source mode alongside the existing SHA256 cert ID
and filesystem path: literal PEM content. CertificateManager.List() (and
therefore CertPool()) now detects inline PEM input via IsPEMContent and
parses it in-process, caching the result under an "embedded-pem-<sha256>"
key.

This enables operators to pre-substitute certificates from KV/secret stores
(Vault, K8s Secrets) at API-load time without persisting private material
in Redis. The change is fully backward-compatible: existing cert-ID and
file-path flows are unchanged and all pre-existing tests pass.

In-scope fields (all route through List() / CertPool() — no per-field
changes required):
  - client_certificates (mTLS allowlist)
  - certificates (per-API domain server cert)
  - upstream_certificates (upstream mTLS, per-host map values)
  - global HttpServerOptions.SSLCertificates
  - Security.Certificates.ControlAPI (CA pool)

Out of scope (per ticket): pinned_public_keys (already accepts content),
KV/secret reference resolution (follow-up), encryption at rest.

Implementation notes:
  - Embedded PEM parse failures append nil to the result (same shape as
    file/Redis failures); callers already handle nil entries.
  - Error logging deliberately omits the PEM body to avoid leaking
    private keys.
  - certUsageTracker (RPC/MDCB sync map) excludes embedded PEM entries
    via IsPEMContent — they are inline content, not sync targets, and
    holding multi-KB strings as map keys would bloat the gateway.

Tests:
  - tyk/certs/manager_test.go: TestList_EmbeddedPEM with 10 subtests
    (single cert, combined cert+key, mixed batch with file/storageID/PEM,
    leading whitespace, CRLF, malformed, truncated, content-hash cache,
    multi-cert chain, CertPool with embedded CA).
  - tyk/gateway/cert_embedded_pem_test.go (new): 5 end-to-end tests doing
    real TLS handshakes, one per in-scope field, plus a 3-source
    backward-compatibility table test.
  - tyk/gateway/cert_usage_tracker_test.go:
    TestExtractCertificatesFromSpec_SkipsEmbeddedPEM.
@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🎯 Recommended Merge Targets

Based on JIRA ticket TT-17295: Support loading of certificates in gateway with certificate content

Fix Version: Tyk 5.14.0

⚠️ Warning: Expected release branches not found in repository

Required:

  • master - No matching release branches found. Fix will be included in future releases.

📋 Workflow

  1. Merge this PR to master first

@probelabs

probelabs Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

This PR introduces the capability for the Gateway to load TLS certificates directly from PEM-encoded strings embedded within API definitions. This adds a third certificate source, alongside the existing methods of loading from a file path or a certificate store ID (SHA256).

The primary goal is to improve operational security and simplify certificate management by allowing secrets management systems (like Vault) to inject certificate content directly at load time, without needing to persist private keys in Redis.

Key changes include:

  • certs/manager.go: The CertificateManager.List() method is updated to detect if a given string is PEM content (using a new IsPEMContent helper). If so, it parses, validates, and caches the certificate in-memory. This includes a fallback to normalize PEMs that have had their line breaks collapsed into a single line.
  • gateway/cert_usage_tracker.go: The certificate usage tracker, which identifies certificates for synchronization, is modified to explicitly ignore these embedded PEM strings. Since they are inline content, they do not need to be fetched or synced.
  • gateway/api_definition.go: The secret substitution logic (for env://, vault://, etc.) is enhanced to correctly JSON-escape multiline values. This is critical to ensure that injected PEM blocks do not invalidate the API definition's JSON structure.
  • Testing: The PR is supported by extensive tests, including new unit tests in certs/manager_test.go and a new suite of end-to-end integration tests in gateway/cert_embedded_pem_test.go that validate the functionality across all affected certificate fields (server certs, mTLS, upstream certs, etc.).

Files Changed Analysis

The changes span 8 files, with a significant number of additions (920) versus deletions (26). This ratio is driven by the addition of comprehensive tests, which account for the majority of the new code.

  • Core Logic (certs/manager.go): Contains the primary feature implementation, adding the new branch to detect, parse, and cache inline PEM content.
  • Testing (certs/manager_test.go, gateway/cert_embedded_pem_test.go): The bulk of the changes. A new integration test file (cert_embedded_pem_test.go) provides robust end-to-end validation for all affected certificate use cases. Unit tests in manager_test.go cover edge cases like malformed PEMs, whitespace, and caching logic.
  • Supporting Changes (gateway/api_definition.go, gateway/util.go): A crucial enhancement was made to JSON-escape secrets during substitution, ensuring that multiline PEM values don't break the API definition's structure.
  • System Integration (gateway/cert_usage_tracker.go): The certificate usage tracker is updated to ignore inline PEMs, correctly identifying them as non-syncable, ephemeral content.

Architecture & Impact Assessment

  • What this PR accomplishes: It decouples certificate loading from persistent storage (filesystem or Redis) by allowing certificates to be provided as ephemeral, inline content within an API definition. This is a foundational step for enabling seamless integration with external secret management systems.

  • Key technical changes introduced:

    1. A new certificate source type (inline PEM) is added to CertificateManager.List() and CertPool().
    2. Parsed inline PEMs are cached in-memory, with a cache key derived from a hash of the PEM content.
    3. Secret substitution logic is updated to handle multiline strings by properly escaping them for JSON.
    4. The certificate usage tracker for MDCB sync is updated to exclude these inline certificates.
  • Affected system components: The change impacts all systems that resolve certificates through the CertificateManager:

    • Gateway TLS Termination (Global SSLCertificates and Per-API certificates)
    • Mutual TLS (mTLS) for client authentication (client_certificates)
    • Upstream mTLS (upstream_certificates)
    • Control API mTLS (Security.Certificates.ControlAPI)
    • Pinned Public Keys (pinned_public_keys)

Certificate Loading Flow

graph TD
    A[API Definition Cert Field] --> B["CertificateManager.List()"];
    B --> C{Is it PEM content?};
    C -- Yes --> D[Parse & Cache In-Memory];
    C -- No --> E{Is it a file path?};
    E -- Yes --> F[Load from Filesystem];
    E -- No --> G["Fetch from Storage (Redis)"];
    D --> H[Return tls.Certificate];
    F --> H;
    G --> H;

Loading

Scope Discovery & Context Expansion

The scope is well-contained within the gateway's certificate management and API loading logic. The author has correctly identified and addressed the downstream impact on the cert_usage_tracker, preventing large PEM strings from being treated as syncable certificate IDs.

The related changes in gateway/api_definition.go to handle JSON escaping for secrets are critical for the feature to work with secret substitution (vault://, env://), which is a primary use case. This demonstrates a thorough understanding of the broader context. The PR effectively enables a future where secret resolvers can fetch a PEM from a vault and inject it directly into the API definition before it's loaded by the gateway.

References

  • certs/manager.go: Core implementation of embedded PEM handling.
  • gateway/cert_embedded_pem_test.go: End-to-end tests demonstrating usage in various certificate fields.
  • gateway/cert_usage_tracker.go: Exclusion of PEM content from the certificate usage map.
  • gateway/api_definition.go: Secret substitution logic updated for multiline values.
Metadata
  • Review Effort: 3 / 5
  • Primary Label: feature

Powered by Visor from Probelabs

Last updated: 2026-06-11T12:24:14.902Z | Triggered by: pr_updated | Commit: 3d34818

💡 TIP: You can chat with Visor using /visor ask <your question>

@probelabs

probelabs Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Security Issues (2)

Severity Location Issue
🟡 Warning certs/manager.go:281
The `IsPEMContent` function uses `strings.Contains` to detect if a string is a PEM block. This is intentionally permissive but can lead to false positives. A file path containing the substring `-----BEGIN ` would be incorrectly identified as inline PEM content. While this would likely result in a non-exploitable parse error, it could prevent a valid certificate from loading and cause a denial of service for that specific API.
💡 SuggestionConsider making the check more specific by trimming whitespace and checking for the prefix, which is still compatible with the goal of allowing leading whitespace. For example: `return strings.HasPrefix(strings.TrimSpace(value), "-----BEGIN ")`.
🟡 Warning certs/manager.go:697
The `normalizeCollapsedPEM` function uses a regular expression to find all PEM blocks in the input via `FindAllSubmatch` without any limits. If a user with privileges to define an API provides a malformed certificate string containing a very large number of PEM-like blocks, this call could consume significant CPU and memory, leading to a denial of service during API loading. This function is a fallback, triggered when standard PEM parsing fails, making it a target for adversarially crafted inputs.
💡 SuggestionIntroduce a limit on the number of PEM blocks processed by this function. A typical certificate or chain will not contain an excessive number of blocks. Limiting the number of matches to a reasonable upper bound (e.g., 32) would mitigate the risk of resource exhaustion. You can pass a limit as the second argument to `FindAllSubmatch`.

Architecture Issues (1)

Severity Location Issue
🟡 Warning certs/manager.go:641-645
A malformed embedded PEM certificate will be re-parsed and trigger an error log on every API reload cycle. When parsing fails, the result is not cached, leading to repeated work and potential log spam for a persistently misconfigured API definition.
💡 SuggestionTo prevent re-parsing known-bad PEMs, implement negative caching. When a parse error occurs for a given PEM content hash, store a sentinel error value or a special nil marker in the cache. On subsequent lookups for the same content hash, if the sentinel is found, immediately return a nil certificate without attempting to parse again.

Performance Issues (1)

Severity Location Issue
🟡 Warning certs/manager.go:106
Malformed embedded PEM certificates are not negatively cached, leading to repeated parsing attempts on each API reload. When parsing an embedded PEM fails, the failure is logged, but no result is stored in the cache. Consequently, if an API definition containing the malformed PEM is reloaded, the gateway will re-run the entire parsing and normalization logic, consuming unnecessary CPU and generating redundant log entries.
💡 SuggestionImplement negative caching for parsing failures. After a PEM fails to parse, store a sentinel value (e.g., a specific error struct or a pre-defined marker) in the cache for that PEM's content hash. In `appendEmbeddedPEM`, before attempting to parse, check the cache for this sentinel and if found, return immediately without re-parsing.

Security Issues (2)

Severity Location Issue
🟡 Warning certs/manager.go:281
The `IsPEMContent` function uses `strings.Contains` to detect if a string is a PEM block. This is intentionally permissive but can lead to false positives. A file path containing the substring `-----BEGIN ` would be incorrectly identified as inline PEM content. While this would likely result in a non-exploitable parse error, it could prevent a valid certificate from loading and cause a denial of service for that specific API.
💡 SuggestionConsider making the check more specific by trimming whitespace and checking for the prefix, which is still compatible with the goal of allowing leading whitespace. For example: `return strings.HasPrefix(strings.TrimSpace(value), "-----BEGIN ")`.
🟡 Warning certs/manager.go:697
The `normalizeCollapsedPEM` function uses a regular expression to find all PEM blocks in the input via `FindAllSubmatch` without any limits. If a user with privileges to define an API provides a malformed certificate string containing a very large number of PEM-like blocks, this call could consume significant CPU and memory, leading to a denial of service during API loading. This function is a fallback, triggered when standard PEM parsing fails, making it a target for adversarially crafted inputs.
💡 SuggestionIntroduce a limit on the number of PEM blocks processed by this function. A typical certificate or chain will not contain an excessive number of blocks. Limiting the number of matches to a reasonable upper bound (e.g., 32) would mitigate the risk of resource exhaustion. You can pass a limit as the second argument to `FindAllSubmatch`.
\n\n ### Architecture Issues (1)
Severity Location Issue
🟡 Warning certs/manager.go:641-645
A malformed embedded PEM certificate will be re-parsed and trigger an error log on every API reload cycle. When parsing fails, the result is not cached, leading to repeated work and potential log spam for a persistently misconfigured API definition.
💡 SuggestionTo prevent re-parsing known-bad PEMs, implement negative caching. When a parse error occurs for a given PEM content hash, store a sentinel error value or a special nil marker in the cache. On subsequent lookups for the same content hash, if the sentinel is found, immediately return a nil certificate without attempting to parse again.
\n\n \n\n

Quality Issues (3)

Severity Location Issue
🟠 Error gateway/api_definition.go:573-623
The functions `replaceConsulSecrets` and `replaceVaultSecrets` modify their string input via a pointer (`*string`), which creates a side effect. This is inconsistent with `replaceSecrets`, which takes a value (`[]byte`) and returns a new value. Functions should ideally not modify input parameters via pointers unless for significant performance reasons, and the inconsistency here makes the API harder to understand and maintain.
💡 SuggestionRefactor the function signatures to return the modified string and an error, for example: `func (a APIDefinitionLoader) replaceVaultSecrets(input string) (string, error)`. This makes the data flow explicit, removes side effects, and improves consistency across the secret replacement functions.
🟡 Warning certs/manager.go:619-654
The function `appendEmbeddedPEM` does not cache parsing failures. If an API definition contains a malformed embedded PEM, it will be re-parsed and a failure will be logged on every API reload. This can lead to performance degradation and log spam for a known-bad configuration.
💡 SuggestionImplement negative caching. When parsing fails, store a specific sentinel value or error in the cache for that PEM's content hash. Subsequent calls would hit the cache and immediately know the PEM is invalid without attempting to re-parse.
🟡 Warning gateway/api_definition.go:538-549
The secret replacement functions (`replaceSecrets`, `replaceConsulSecrets`, `replaceVaultSecrets`) use `strings.ReplaceAll` inside a loop. This repeatedly scans the entire API definition string for each secret, which is inefficient for large definitions with many secrets.
💡 SuggestionUse `strings.NewReplacer` to perform all replacements in a single pass. Collect all old/new string pairs into a slice and create a `Replacer` to apply them at once, which is significantly more performant.

Powered by Visor from Probelabs

Last updated: 2026-06-11T12:24:07.027Z | Triggered by: pr_updated | Commit: 3d34818

💡 TIP: You can chat with Visor using /visor ask <your question>

mativm02 added 2 commits May 29, 2026 09:19
- Restore tyklog.Get() default logger in NewCertificateManager and
  NewSlaveCertManager. The earlier patch unintentionally swapped this
  to logrus.New() during a cross-branch file copy; reverting to keep
  certs/manager.go consistent with the rest of the codebase.
- Extract the embedded-PEM cache+parse logic out of List() into a new
  helper, certificateManager.appendEmbeddedPEM, using the safe
  comma-ok form for the cache type assertion. Resolves the errcheck
  warnings SonarCloud raised on the inline branch and keeps List()'s
  cognitive complexity stable.
- Split TestList_EmbeddedPEM (10 t.Run subtests, cognitive complexity
  37) into 10 focused top-level test functions sharing a
  requireSingleCert helper. Behaviour is identical; SonarCloud's
  15-complexity ceiling is satisfied.
- Fix lint findings in gateway/cert_embedded_pem_test.go:
    * rename unused 'info *tls.CertificateRequestInfo' parameter to '_'
      (revive: unused-parameter).
    * handle errors from x509.ParseCertificate, url.Parse, and w.Write
      (errcheck).

No production behaviour change beyond the helper extraction. All
existing cert + TLS gateway tests still pass.
staticcheck QF1012 raised on the upstream handler in
TestEmbeddedPEM_UpstreamCertificates. fmt.Fprintf is the canonical
pattern for writing formatted output to an io.Writer and avoids the
intermediate byte slice.

@kofoworola kofoworola left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add a test that creates an api using the certificate directly embedded in the api definition and ensure the gateway works

@mativm02

mativm02 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@kofoworola — yes, that's exactly what gateway/cert_embedded_pem_test.go does. Each test spins up the full gateway via StartTest, builds an API definition with the PEM embedded directly into the relevant field, loads it via BuildAndLoadAPI / LoadAPI, then drives a real HTTP/TLS request through the gateway with ts.Run and asserts behaviour. No file paths, no CertificateManager.Add() calls — the PEM lives in the API definition itself.

Specifically:

  • TestEmbeddedPEM_ClientCertificates — API def with spec.ClientCertificates = []string{string(clientCertPem)}, spec.UseMutualTLSAuth = true. Asserts the gateway accepts a client signed by the embedded CA and rejects a client without a cert.
  • TestEmbeddedPEM_UpstreamCertificates — API def with spec.UpstreamCertificates = map[string]string{\"*.target:<port>\": string(combinedClientPEM)} proxying to an mTLS-protected upstream. Asserts the upstream handshake succeeds using the embedded client PEM.
  • TestEmbeddedPEM_DomainCertificates — API def with spec.Certificates = []string{string(combinedPEM)} for per-API TLS termination. Asserts a TLS client gets 200 OK.
  • TestEmbeddedPEM_GlobalSSLCertificates — global HttpServerOptions.SSLCertificates = []string{string(combinedPEM)}. Asserts the gateway boots with the embedded server cert and serves HTTPS.
  • TestEmbeddedPEM_ControlAPIClientCAs — global Security.Certificates.ControlAPI = []string{string(clientCertPem)} (exercises CertPool() rather than just List()). Asserts the management API accepts a client signed by the embedded CA.
  • TestEmbeddedPEM_BackwardCompatibility — a table test that resolves the same cert through all three sources (file path, cert ID via Add(), embedded PEM) and asserts they coexist.

Happy to add more coverage if you want a specific scenario that isn't there — e.g. an OAS API definition path, or a hot-reload scenario where an embedded PEM changes on LoadAPI.

@bojank93

bojank93 commented Jun 4, 2026

Copy link
Copy Markdown

QA review by Quinn · TT-17295 · PR #8256 · 2026-06-04


PR Summary

PR #8256 adds a third certificate-source mode to CertificateManager.List() and CertPool(): inline PEM content. The change touches certs/manager.go (new IsPEMContent helper, appendEmbeddedPEM method, and the embedded-PEM branch inside List()) and gateway/cert_usage_tracker.go (excludes raw PEM strings from MDCB cert-sync tracking). Backward compatibility is preserved — existing cert-ID and file-path flows are untouched. The PR aligns with TT-17295 and its three acceptance criteria.


Jira Alignment

Field Value
Ticket TT-17295 — Support loading of certificates in gateway with certificate content
Type Story
Status In Test
Acceptance Criteria Met? ⚠️ Partial

Criteria check:

# Acceptance Criterion Covered in PR? Note
1 PEM cert in any cert field in Tyk Classic or Tyk OAS API definitions → used in TLS handshake ⚠️ Partial cert_embedded_pem_test.go names suggest Classic-style fields (client_certificates, upstream_certificates, etc.). OAS-style API definitions not explicitly confirmed in test names or PR description. Infrastructure is shared, so risk is low, but the ticket explicitly names both styles.
2 Cert ID from cert store → backward-compatible ✅ Yes TestEmbeddedPEM_BackwardCompatibility table test covers cert-ID source mode.
3 File path → backward-compatible ✅ Yes TestEmbeddedPEM_BackwardCompatibility table test covers file-path source mode.

Unit & Integration Test Coverage

Package Coverage Tests Added? Gap Description Severity
github.com/TykTechnologies/tyk/certs 50.9% pre-merge baseline Yes — 10 unit tests No negative caching for malformed PEM; same bad input re-parsed and re-logged on every reload. Low severity — cert loading is not per-request. 🟢 Low
gateway (cert_embedded_pem) Integration only Yes — 6 e2e tests OAS API definition style not confirmed tested 🟡 Medium
gateway (cert_usage_tracker) Partial Yes — 1 test ✅ None

Test quality notes:

  • Naming follows TestFunctionName_Scenario convention throughout. ✅
  • Table-driven pattern used for TestEmbeddedPEM_BackwardCompatibility. ✅
  • TestList_EmbeddedPEM_MalformedBodyReturnsNil and TestList_EmbeddedPEM_TruncatedReturnsNil explicitly test the nil-sentinel error path. ✅
  • TestList_EmbeddedPEM_CacheKeyUsesContentHash validates cache key uniqueness by content. ✅

UI Test Coverage (Playwright)

No frontend files changed — section not applicable.


Security Findings

# Severity Location Finding Recommendation
1 🟡 Medium CI — repo-wide SentinelOne CNS scan failed with 24 CVEs. None are introduced by this PR. All findings are in pre-existing dependencies: handlebars 4.7.8 (Critical, CVE-2026-33937), node-forge 1.3.1 (multiple High), axios 1.15.0 (multiple High), flatted 3.2.6 (High), lodash 4.17.21 (High), underscore 1.12.1 (High), fast-uri 3.1.0 (High), github.com/docker/docker v28.5.2 (multiple High). The scanner performs a full-repo dependency scan, not a diff-scoped scan. Track in a dedicated dependency-update PR. Not a blocker for this change.

PR-specific security review (no issues found):

  • PEM content is NOT logged on parse failure — appendEmbeddedPEM logs only the error, not the PEM body. PR description explicitly calls this out. ✅
  • IsPEMContent substring match is permissive by design; ParsePEMCertificate performs strict downstream validation. The comment documenting why false positives are safe is clear and will survive future readers. ✅
  • embeddedPEMCacheKeyPrefix = "embedded-pem-" contains - which disqualifies it from being a hex-only SHA256 cert ID, eliminating org-prefixed cache shadow attacks. ✅
  • SHA256 of trimmed PEM content as cache key: content-addressed deduplication across API definitions; collision risk is computationally infeasible. ✅
  • Embedded PEM correctly excluded from MDCB cert-sync tracker — raw PEM strings would bloat the sync map with multi-KB keys and have no sync target. ✅
  • Cache Set/Get is mutex-guarded — concurrent List() calls for the same PEM at worst duplicate one parse then converge. ✅

Code Quality Issues

# Severity Location Issue Recommendation
1 🟢 Low certs/manager.goappendEmbeddedPEM Parse failures skip cache.Set. A misconfigured API definition with a malformed embedded PEM will re-parse and re-log an error on every API reload cycle. Cache a typed sentinel (e.g. a parsedCertError wrapper) to short-circuit repeated failures and suppress log spam after the first occurrence.

All other quality checklist items pass: errors use WithError(err) via logrus (consistent with codebase style), new constant is documented, no goroutine leaks, no fmt.Println.


Tyk Domain Issues

# Severity Location Issue Recommendation
1 🟡 Medium gateway/cert_embedded_pem_test.go Integration tests cover Classic-style fields. OAS API definition style not confirmed exercised. The ticket acceptance criterion 1 explicitly names "Tyk Classic and Tyk OAS." The infrastructure path (CertificateManager.List()) is shared, so the risk of a functional gap is low — but coverage against an OAS API definition is still worth one test or a comment confirming the shared path is sufficient. Add one BuildAPI call with an OAS-style definition (or add a note in the PR body confirming OAS routes through the same List() call and no OAS-specific cert-field handling exists).

What Was Done Well

  • Comprehensive unit tests with idiomatic subtests: 10 t.Run subtests cover every meaningful variant — happy path, combined cert+key PEM, mixed batch (PEM + cert ID + file path), whitespace tolerance, CRLF normalization, malformed input, truncated input, content-hash caching, multi-cert chain, and CertPool integration. This is above the bar for new caching/parsing code.
  • Content-addressed caching design: SHA256 of PEM content as cache key deduplicates shared embedded certs across multiple API definitions using the same material. Correct and efficient.
  • Proactive safety documentation in the PR description: the author explicitly addresses detection ambiguity, private-key logging safety, cache-key collision analysis, and concurrency — before being asked. This is the standard.
  • Correct exclusion from MDCB cert usage tracker: embedded PEM strings carry no cert-store ID and would produce multi-KB unusable sync entries. Filtering them with IsPEMContent at the tracker boundary is exactly right.
  • Backward-compatibility integration test: TestEmbeddedPEM_BackwardCompatibility uses a three-row table test — file path, cert ID, embedded PEM — that directly maps to all three acceptance criteria in a single test function.

Gap Summary

Area Gaps Found Highest Severity
Unit / Integration Tests 1 🟡 Medium (OAS coverage unconfirmed)
UI Tests (Playwright) N/A
Security 1 🟡 Medium (pre-existing dep CVEs — not PR-introduced)
Code Quality 1 🟢 Low (no negative caching for parse failures)
Tyk Domain 1 🟡 Medium (OAS API definition not explicitly tested)
Jira Alignment 1 criterion unconfirmed ⚠️ Partial

Verdict

NEEDS DISCUSSION

The implementation is clean and well-reasoned. The SentinelOne failure is entirely pre-existing and not a concern for this PR. The one open question before approving: confirm that cert_embedded_pem_test.go exercises at least one OAS-style API definition — the Jira acceptance criterion explicitly names "Tyk Classic and Tyk OAS." Because CertificateManager.List() is shared infrastructure, the risk of an actual functional gap is low, but OAS test coverage (or a clear note explaining why Classic coverage is sufficient here) is needed to mark criterion 1 fully met. If the author can confirm this — either by pointing to the relevant BuildAPI call or adding a single OAS-flavoured test variant — this can move straight to Approve.


Review by Quinn · Tyk QA · 2026-06-04

mativm02 and others added 6 commits June 9, 2026 09:57
… PEM

When a certificate is pasted into a single-line text field (e.g. the
Dashboard Raw editor) its line breaks get collapsed into spaces, which
Go's pem.Decode rejects with "Can't find CERTIFICATE block". This is a
common copy-paste mishap that QA hit immediately.

Add a normalization fallback in certs.appendEmbeddedPEM: when the first
ParsePEMCertificate fails, rebuild a canonical PEM (strip intra-body
whitespace, re-wrap base64 at 64 columns, markers on their own lines)
and retry once. The fallback runs ONLY after a normal parse fails, so
well-formed input is never reshaped, and genuinely malformed input still
returns nil (the retry parse fails too). A Warn is logged so operators
know their formatting was non-canonical.

Handles multi-block input (cert + private key) via a non-greedy per-block
regex, so upstream_certificates (combined cert+key) are covered too.

Tests:
  - certs/manager_test.go: TestList_EmbeddedPEM_SingleLineCollapsed and
    TestList_EmbeddedPEM_SingleLineCombinedCertKey.
  - gateway/cert_embedded_pem_test.go: TestEmbeddedPEM_SingleLineClientCertificate
    drives a full mTLS handshake through StartTest using a single-line
    client cert in the API definition (mirrors QA's exact scenario).
  - Existing malformed/truncated tests still assert nil, confirming the
    fallback does not rescue genuinely broken input.
…escapes chars (#8300)

<!-- Provide a general summary of your changes in the Title above -->

## DEMO
Current KVs can't resolve values with unescapes chars. Given an api with
the vault referecing cert value without removed any chars. Then gateway
process is started and error happens on loading APIs and resolving
references. Then switch branch to current and trying do the same.


https://github.com/user-attachments/assets/1677a0aa-e8e7-460c-8951-b7ee9b2b3a81




## Related Issue

<!-- This project only accepts pull requests related to open issues. -->
<!-- If suggesting a new feature or change, please discuss it in an
issue first. -->
<!-- If fixing a bug, there should be an issue describing it with steps
to reproduce. -->
<!-- OSS: Please link to the issue here. Tyk: please create/link the
JIRA ticket. -->

## Motivation and Context

<!-- Why is this change required? What problem does it solve? -->

## How This Has Been Tested

<!-- Please describe in detail how you tested your changes -->
<!-- Include details of your testing environment, and the tests -->
<!-- you ran to see how your change affects other areas of the code,
etc. -->
<!-- This information is helpful for reviewers and QA. -->

## Screenshots (if appropriate)

## Types of changes

<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Refactoring or add test (improvements in base code or adds test
coverage to functionality)

## Checklist

<!-- Go over all the following points, and put an `x` in all the boxes
that apply -->
<!-- If there are no documentation updates required, mark the item as
checked. -->
<!-- Raise up any additional concerns not covered by the checklist. -->

- [ ] I ensured that the documentation is up to date
- [ ] I explained why this PR updates go.mod in detail with reasoning
why it's required
- [ ] I would like a code coverage CI quality gate exception and have
explained why

---------

Co-authored-by: Vlad Zabolotnyi <[email protected]>
ListPublicKeys/ListRawPublicKey previously branched only on
isSHA256(id) -> Redis, else -> ReadFile, so a pasted PEM public key was
treated as a filename ('file name too long'). Add an inline-PEM branch
(IsPEMContent) that decodes the key in memory, mirroring List().

- Shared decodePublicKeyBlock helper with normalizeCollapsedPEM fallback
  so single-line/whitespace-collapsed pasted keys are tolerated.
- Bounded, content-hashed cache key for inline PEM (publicKeyCacheKey),
  consistent with embeddedPEMCacheKeyPrefix used by List().
- Unit tests (inline, whitespace, single-line collapsed, raw, malformed)
  and a full-gateway pinned-key handshake test.
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Jira Linter Failed

Commit: 3d34818
Failed at: 2026-06-11 12:22:37 UTC

The Jira linter failed to validate your PR. Please check the error details below:

🔍 Click to view error details
failed to get Jira issue: failed to fetch Jira issue TT-17295: Issue does not exist or you do not have permission to see it.: request failed. Please analyze the request body for more details. Status code: 404

Next Steps

  • Ensure your branch name contains a valid Jira ticket ID (e.g., ABC-123)
  • Verify your PR title matches the branch's Jira ticket ID
  • Check that the Jira ticket exists and is accessible

This comment will be automatically deleted once the linter passes.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
0 Accepted issues

Measures
0 Security Hotspots
88.8% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@mativm02
mativm02 merged commit 6963234 into master Jun 12, 2026
33 of 55 checks passed
@mativm02
mativm02 deleted the feat/TT-17295/embedded-pem-certs branch June 12, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants