[TT-17295] feat: support embedded PEM certificate content in Gateway#8256
Conversation
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.
🎯 Recommended Merge TargetsBased on JIRA ticket TT-17295: Support loading of certificates in gateway with certificate content Fix Version: Tyk 5.14.0
Required:
📋 Workflow
|
|
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:
Files Changed AnalysisThe 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.
Architecture & Impact Assessment
Certificate Loading Flowgraph 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;
Scope Discovery & Context ExpansionThe 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 The related changes in References
Metadata
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 |
Security Issues (2)
Architecture Issues (1)
Performance Issues (1)
Security Issues (2)
Quality Issues (3)
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 |
- 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
left a comment
There was a problem hiding this comment.
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
|
@kofoworola — yes, that's exactly what Specifically:
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 |
|
QA review by Quinn · TT-17295 · PR #8256 · 2026-06-04 PR SummaryPR #8256 adds a third certificate-source mode to Jira Alignment
Criteria check:
Unit & Integration Test Coverage
Test quality notes:
UI Test Coverage (Playwright)No frontend files changed — section not applicable. Security Findings
PR-specific security review (no issues found):
Code Quality Issues
All other quality checklist items pass: errors use Tyk Domain Issues
What Was Done Well
Gap Summary
VerdictNEEDS 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 Review by Quinn · Tyk QA · 2026-06-04 |
… 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.
🚨 Jira Linter FailedCommit: The Jira linter failed to validate your PR. Please check the error details below: 🔍 Click to view error detailsNext Steps
This comment will be automatically deleted once the linter passes. |
|



Summary
CertificateManager.List()/CertPool(): literal PEM content (in addition to the existing SHA256 cert ID and filesystem path modes).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 helperIsPEMContent(string) bool, new private constantembeddedPEMCacheKeyPrefix = "embedded-pem-", and an embedded-PEM branch at the top ofList(). Also adds a clarifying comment onListPublicKeys()documenting the deliberate omission for pinned public keys (already accepts content per ticket).tyk/gateway/cert_usage_tracker.go—extractCertificatesFromSpecnow skips inline PEM values viacerts.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.go—TestList_EmbeddedPEMwith 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,CertPoolwith 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, globalSSLCertificates, 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.go—TestExtractCertificatesFromSpec_SkipsEmbeddedPEM.In-scope cert fields
All route through
CertificateManager.List()/CertPool(), so the single change inmanager.golights all of them up transparently:client_certificates(mTLS allowlist)gateway/mw_certificate_check.go:111certificates(per-API domain server cert)gateway/cert.go:535upstream_certificates(per-host map values)gateway/cert.go:141HttpServerOptions.SSLCertificatesgateway/cert.go:424Security.Certificates.ControlAPI(CA pool)gateway/cert.go:472→CertPool()Out of scope (follow-up tickets)
pinned_public_keys— already accepts content (fingerprints) per ticket; no change needed.vault://,secret-ref://) — pre-resolution happens before values reachCertificateManager.internal/httpclient/factory.go'sMTLS.CAFile(separate internal-services config path, not in ticket's field list).Safety notes
IsPEMContentmatches"-----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.Debug("Failed certificate: ", string(rawCert))log is unreachable for embedded-PEM input because every embedded-PEM path terminates the loop iteration withcontinue.embedded-pem-prefix contains a-and is unlikely to be addressable as anorg-id + cert-idconcatenation, eliminating the theoretical operator-controlled cache shadow.Set/Getis mutex-guarded; two parallelList()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 newTestList_EmbeddedPEM).go test ./tyk/gateway/ -run 'Cert|TLS|Mutual|Embedded|Pinned|ClientCertif|UpstreamCert|ExtractCertificatesFromSpec'— all cert/TLS-adjacent gateway tests pass.TestCertificateStorage(file + Redis paths) andTestUpstreamMutualTLS/TestAPIMutualTLS/TestGatewayTLS/TestAPICertificate/TestGatewayControlAPIMutualTLS/TestCertificateHandlerTLScontinue to pass (backward-compat regression check).go vet ./tyk/certs/... ./tyk/gateway/...clean.go build ./tyk/...clean.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
Generated at: 2026-05-29 13:16:32