Envoy Gateway Configuration to Reach SSL Labs A+
2026-05-14
This article documents the Envoy Gateway configuration required to achieve an A+ rating on Qualys SSL Labs, with the rationale for each setting.
1. TLS security background
Before any application data flows over https://, the TLS session completes two steps:
- Authentication. The server presents an X.509 certificate signed by a trusted Certificate Authority.
- Key agreement and encryption. Client and server negotiate a session key via a cipher suite (key exchange, signature, bulk cipher, MAC) and encrypt the channel.
Weaknesses in this chain enable specific attack classes:
- Deprecated protocol versions (SSL 3, TLS 1.0, TLS 1.1) are vulnerable to POODLE, BEAST, and downgrade attacks, and rely on broken primitives such as RC4 and MD5-HMAC.
- Weak cipher suites (RC4, 3DES, CBC modes with HMAC-SHA1, anything without forward secrecy) leak data or enable padding-oracle attacks.
- Missing forward secrecy allows a single compromised private key to decrypt all previously recorded traffic.
- Missing HSTS allows a network attacker to strip the
https://redirect on first visit and serve plain HTTP. - Certificate hygiene issues (weak keys, SHA-1 signatures, missing intermediates) break the trust chain.
An A+ configuration eliminates these weaknesses while preserving interoperability with modern clients.
2. The SSL Labs grading methodology
Qualys SSL Labs is a remote scanner that probes a public HTTPS endpoint and grades it from F to A+. The grade is derived from four scored categories combined with overriding hard rules.
| Category | Weight | Measures |
|---|---|---|
| Certificate | 30% | Trust chain, key size, signature algorithm, expiry, hostname match |
| Protocol Support | 30% | Enabled and disabled TLS versions |
| Key Exchange | 30% | Strength of DH/ECDH parameters and forward secrecy |
| Cipher Strength | 10% | The weakest cipher suite the server will accept |
Overriding hard rules:
- TLS 1.0 or 1.1 enabled -> grade capped at B.
- TLS 1.3 not supported -> grade capped at A- (TLS 1.3 is graded as 100% protocol strength).
- HSTS absent or invalid -> grade reduced from A to A-.
- No forward secrecy -> capped at B.
- No AEAD ciphers -> capped at B.
An A+ grade requires an A-grade configuration with no warnings and a valid Strict-Transport-Security header with max-age of at least six months (15 768 000 seconds).
3. Configuration requirements
Each item below maps directly to a YAML field in the following sections.
- TLS 1.2 and 1.3 only. All earlier versions disabled. TLS 1.3 must be enabled.
- AEAD cipher suites with forward secrecy only:
ECDHEkey exchange withAES-GCMorChaCha20-Poly1305. No CBC, RC4, or 3DES. - Strong certificate: 2048-bit RSA minimum or 256/384-bit ECDSA, SHA-256 signature, full chain including intermediates.
- HSTS on every HTTPS response with
max-age >= 15 768 000, preferably withincludeSubDomains. Inject viaClientTrafficPolicy.headers.lateResponseHeadersso the header is present on backend responses and on Envoy-generated error responses. - HTTP-to-HTTPS redirect on port 80.
- CAA DNS record restricting which CAs may issue certificates for the domain. Not directly scored, but a recommended practice.
4. The Envoy Gateway resources
The complete A+ configuration consists of four resources:
- A
Secretcontaining the full chain and key – produced by cert-manager (see section 5). - A
Gatewaywith HTTP (port 80, redirect) and HTTPS (port 443) listeners, annotated to drive cert-manager. - A
ClientTrafficPolicyscoped to the HTTPS listener that pins TLS 1.2/1.3 with AEAD-only forward-secret ciphers, modern curves, ALPN, and safe signature algorithms, and injectsStrict-Transport-Securityand related security headers gateway-wide viaheaders.lateResponseHeaders. - Two
HTTPRouteresources – one to 301-redirect HTTP to HTTPS, one to forward traffic to the backend.
Combined with cert-manager using DNS-01 issuance, the full set of hardening annotations, and a CAA record, this configuration satisfies SSL Labs A+ and the transport-layer requirements of PCI-DSS 4.0, ISO 27001:2022, and comparable frameworks.
The diagram below shows how the resources interact, including cert-manager populating the Secret out of band:
flowchart LR
Client((Client))
subgraph Gateway["Gateway: eg"]
HTTP["listener http :80"]
HTTPS["listener https :443<br/>TLS Terminate"]
end
Redirect["HTTPRoute<br/>http -> https (301)"]
App["HTTPRoute<br/>app"]
CTP["ClientTrafficPolicy<br/>=============<br/>TLS 1.2/1.3 + ciphers<br/>Late HSTS header"]
Secret[("Secret<br/>johanneskueber-com-tls")]
Backend[("Backend Service")]
Client -->|":80"| HTTP
Client -->|":443"| HTTPS
HTTP --> Redirect
Redirect -.->|"301 Location"| Client
HTTPS --> App --> Backend
CTP -.->|"sectionName: https"| HTTPS
Secret -.->|"certificateRefs"| HTTPS
CM["cert-manager<br/>gateway-shim"] -.->|"creates / renews"| Secret
CI["ClusterIssuer<br/>Let's Encrypt DNS-01"] --> CM
Gateway -.->|"cluster-issuer annotation"| CM
The examples assume Envoy Gateway v1.5+ and the Gateway API standard channel (gateway.networking.k8s.io/v1).
4.1 The TLS certificate Secret
| |
Field reference:
type: kubernetes.io/tls– the standard Kubernetes TLS secret type. Envoy Gateway requires this exact type; anOpaquesecret with the same keys is rejected by the Gateway API.tls.crt– must contain the full chain: leaf certificate followed by every intermediate up to but not including the root, concatenated in PEM. Serving only the leaf produces an “incomplete chain” warning that caps the grade below A.tls.key– the matching private key. Recommended: ECDSA P-256 or P-384, or 2048-bit RSA at minimum. Anything below 2048-bit RSA fails the certificate category outright.
Envoy Gateway supports OCSP stapling via an additional
tls.ocsp-staplekey in the Secret, but it is intentionally omitted here. Let’s Encrypt removed OCSP URLs from issued certificates in May 2025 and shut down its OCSP responders in August 2025; revocation for Let’s Encrypt-issued certificates is now distributed exclusively via CRLs. Stapling applies only when using a CA that still publishes OCSP responses (DigiCert, Sectigo, private PKI). cert-manager does not populate the staple field, so a separate controller (e.g.ocsp-manager) is required if stapling is needed.
In production the Secret is generated by cert-manager rather than authored manually – see section 5.
4.2 The Gateway: two listeners
| |
The port-80 listener exists solely to host the HTTP-to-HTTPS redirect. SSL Labs and other scanners require the redirect to grade the deployment correctly; without the listener, port 80 returns connection-refused and the redirect is flagged as missing.
Field reference:
gatewayClassName: eg– references theGatewayClassinstalled by the Envoy Gateway Helm chart, identifying the controller responsible for this Gateway.listeners[].name– stable identifier referenced fromClientTrafficPolicyviasectionName, allowing distinct policies per listener.protocol: HTTPS– terminates TLS at the listener and parses HTTP inside the encrypted channel. The alternativeprotocol: TLSperforms SNI-based TCP passthrough, which prevents HTTP-level routing and HSTS injection.port: 443– required by SSL Labs, which does not test arbitrary ports by default.hostname: "www.johanneskueber.com"– restricts the SNI values this listener accepts and allows multiple sites to share a Gateway. Wildcards such as"*.johanneskueber.com"are supported.tls.mode: Terminate– Envoy decrypts at the listener and forwards plaintext (or re-encrypted via aBackendTLSPolicy) to the backend.tls.certificateRefs– one or more Secret references. Multiple Secrets enable dual RSA + ECDSA serving. Cross-namespace references require aReferenceGrant.allowedRoutes.namespaces.from: All– allowsHTTPRouteresources in any namespace to attach. Tighten toSameorSelectorfor stricter multi-tenancy.
This Gateway does not yet enforce TLS 1.2+, restrict cipher suites, or send HSTS. Those are configured next.
4.3 The ClientTrafficPolicy: TLS hardening and gateway-wide security headers
This resource is the core of the A+ rating. ClientTrafficPolicy controls how Envoy behaves toward downstream clients, including TLS parameters and globally injected response headers.
| |
TLS parameters
targetRefs[].sectionName: https– binds the policy to thehttpslistener only. WithoutsectionNamethe policy attaches to every listener on the Gateway.tls.minVersion: "1.2"– refuses handshakes using TLS 1.0 or 1.1, moving the protocol-support score from “capped at B” into A range. Valid values:"1.0","1.1","1.2","1.3". Versions below 1.2 are deprecated by RFC 8996 (2021). The value must be quoted; an unquoted1.3is parsed as a float and rejected.tls.maxVersion: "1.3"– permits TLS 1.3 in negotiation. SSL Labs grades TLS 1.3 at 100% protocol strength and caps the grade at A- if absent. Under TLS 1.3 thecipherslist is ignored; TLS 1.3 has a fixed AEAD-only cipher set (TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256) that cannot be subsetted.tls.ciphers– applies only to TLS 1.2. The list is in server preference order; the first cipher the client also supports wins. Selection rationale:ECDHE-ECDSA-AES256-GCM-SHA384/...-RSA-...– strongest AEAD with ECDHE for forward secrecy. ECDSA variants are listed first because they are faster when the server presents an ECDSA certificate.ECDHE-...-CHACHA20-POLY1305– preferred on mobile clients lacking AES-NI hardware acceleration....-AES128-GCM-SHA256– for compatibility with older clients. AES-128 in GCM mode remains fully secure.- All CBC suites, non-ephemeral RSA key exchange, SHA-1, 3DES, and RC4 are excluded. CBC suites are vulnerable to Lucky13-style timing attacks; non-ephemeral RSA lacks forward secrecy and caps the grade at B.
tls.ecdhCurves– the elliptic curves offered for ECDHE.X25519is the modern default;P-256is included for legacy and FIPS compatibility.P-384andP-521are slower and rarely required.tls.alpnProtocols– Application-Layer Protocol Negotiation.h2advertises HTTP/2;http/1.1is the fallback. Enables HTTP/2 without an additional round-trip.tls.signatureAlgorithms– restricts the signature schemes the server advertises. SHA-1 schemes (rsa_pkcs1_sha1etc.) are excluded; their presence triggers aT(trust) penalty on SSL Labs. Modern RSA-PSS (rsa_pss_rsae_*) and PKCS#1 (rsa_pkcs1_*, for TLS 1.2 clients) are included.
With no
minVersionormaxVersionspecified, Envoy Gateway defaults to TLS 1.2 minimum and TLS 1.3 maximum. The explicit block above documents the contract and adds cipher hardening.
Gateway-wide security headers
headers.lateResponseHeaders is the recommended location for HSTS and other security headers. It runs after all per-route filters and after the backend response, immediately before bytes leave Envoy. Two consequences follow:
- Headers apply to Envoy-generated responses – 503s when the backend is unreachable, 504s on timeout, 400s on malformed requests. Per-route
ResponseHeaderModifierfilters do not run on these responses. - Single source of truth – no need to replicate HSTS on every
HTTPRoute.
Verb selection:
set– overwrites any value sent by the backend. Used forStrict-Transport-Security,X-Content-Type-Options, andReferrer-Policy, where there is one correct value at the gateway edge.addIfAbsent– applied only if the backend did not set the header. Used forContent-Security-PolicyandX-Frame-Options, where individual applications may require a stricter or more permissive policy.add– appends to existing values. Avoided for security headers, where duplicate values produce undefined browser behaviour. Reserved for telemetry and tracing headers.
Strict-Transport-Security directive breakdown:
max-age=63072000– two years. SSL Labs requires at least15768000(6 months) for A+; two years is required for hstspreload.org submission.includeSubDomains– extends the policy to every subdomain. Enable only when every subdomain has a valid certificate and serves HTTPS; otherwise the policy locks legitimate subdomains out formax-ageseconds.preload– signals intent for inclusion in the HSTS preload list. Browsers ship the list pre-installed, so even first-time visitors refuse plain HTTP. Effectively irreversible – submit at hstspreload.org only after running withmax-age=63072000; includeSubDomainsfor several weeks without issue.
4.4 HTTP -> HTTPS redirect
Without this, clients reaching port 80 receive a connection refusal or 404 and never reach the HTTPS site.
| |
Field reference:
parentRefs[].sectionName: http– attaches the route to the HTTP listener only. Applying it to HTTPS would cause HTTPS requests to redirect to themselves.hostnames– restrict to owned domains to prevent open-redirect abuse.filters[].type: RequestRedirect– built-in Gateway API filter; no custom resource required.requestRedirect.scheme: https– destination scheme.requestRedirect.statusCode: 301– permanent redirect, cached by browsers. HSTS preload checkers require a 301.requestRedirect.port: 443– explicit destination port. Envoy can infer it from the scheme, but explicit values avoid ambiguity behind non-standard load balancers.
4.5 The application HTTPRoute
With security headers managed by ClientTrafficPolicy, the application route only needs to forward traffic. Per-route exceptions (such as a stricter CSP for a specific path) can be added via a ResponseHeaderModifier filter; the late-headers policy applies afterwards.
| |
parentRefs[].sectionName: https– attaches the route to the HTTPS listener only.matches[].path.type: PathPrefix, value: /– catch-all match. Replace with more specific matches for finer routing.
5. cert-manager with maximum-security annotations
Two manifests are required: a ClusterIssuer and an annotated Gateway. The cert-manager gateway-shim controller watches Gateways for the trigger annotation and creates and renews Certificate resources automatically; no Certificate resource is authored by the user.
5.1 Precondition
The cert-manager controller must be started with Gateway API support enabled, and the Gateway API CRDs must have been present at startup. In Helm:
| |
This is enabled by default in cert-manager 1.15+. If the Gateway API CRDs were installed after cert-manager, restart its Deployment (kubectl rollout restart deployment cert-manager -n cert-manager); the shim checks for the CRDs only at startup and silently does nothing if they are missing.
5.2 The ClusterIssuer (Let’s Encrypt with DNS-01)
| |
DNS-01 is preferred over HTTP-01 for several reasons:
- The HTTP-01 challenge endpoint on port 80 is not required, removing an attack surface.
- Wildcard certificates (
*.johanneskueber.com) are supported; HTTP-01 cannot issue them. - The challenge transits between cert-manager and the DNS provider API; an attacker controlling the HTTP path cannot answer it.
- The CA validates via public DNS, which is harder to MITM than HTTP from arbitrary validator nodes.
The apiTokenSecretRef must reference a token scoped to DNS edit on the relevant zone only, not a global account token.
The privateKey block configures the ACME account key – separate from the certificate keypair – used to authenticate cert-manager to Let’s Encrypt.
5.3 The annotated Gateway
| |
Trigger annotations
Exactly one of the two annotations below causes cert-manager to watch the Gateway and create a Certificate for each TLS listener’s Secret. No other annotation triggers cert-manager; the remainder are optional tuning applied to the generated Certificate.
cert-manager.io/cluster-issuer: <name>– references a cluster-scopedClusterIssuer. Works from any namespace.cert-manager.io/issuer: <name>– references a namespacedIssuer. Must be in the Gateway’s namespace.
Two additional annotations are required only for out-of-tree issuers (Venafi, AWS PCA, HashiCorp Vault, etc.):
cert-manager.io/issuer-kind: <Kind>cert-manager.io/issuer-group: <api-group>
Hardening annotations
cert-manager.io/private-key-algorithm: ECDSA– ECDSA is preferred over RSA for smaller handshakes and faster signing. 256-bit ECDSA provides cryptographic strength equivalent to ~3072-bit RSA. RSA remains relevant only for compatibility with very old clients (pre-Android 4.0, pre-IE 11, pre-Java 7).cert-manager.io/private-key-size: "384"– P-384 over P-256. P-256 is sufficient for A+; P-384 provides ~192-bit security and aligns with CNSA “Top Secret”. The cost is approximately 2x CPU on the signing operation, negligible at any realistic QPS. The value must be quoted. Use"256"for minimum handshake latency.cert-manager.io/private-key-encoding: PKCS8– modern key encoding. PKCS#1 (cert-manager’s legacy default) is RSA-only; PKCS#8 supports any algorithm.cert-manager.io/private-key-rotation-policy: Always– generates a new keypair at every reissue. WithNever(the legacy default before cert-manager 1.18), the same key persists for the Secret’s lifetime, meaning a leaked key remains valid across renewals. The default flipped toAlwaysin 1.18; pinning it explicitly prevents config drift.cert-manager.io/duration: "2160h"– 90 days, the Let’s Encrypt maximum. Shorter durations limit blast radius on compromise. Private CAs typically support shorter durations (24h-7d is common for service-to-service mTLS).cert-manager.io/renew-before: "720h"– renew 30 days before expiry, approximately one-third of the total lifetime. This buffer absorbs ACME rate-limit hits and DNS propagation delays.cert-manager.io/revision-history-limit: "3"– retain threeCertificateRequestresources. The default in cert-manager 1.18+ is1;3provides a small audit trail without unbounded growth.cert-manager.io/usages: "digital signature,key encipherment,server auth"– restricts the X.509 Extended Key Usage and Key Usage extensions to TLS server use only.client authandcode signingare excluded, so a leaked private key cannot sign client certificates or binaries. Public CAs always setserver auth; this annotation primarily affects private CAs that honour requested usages.cert-manager.io/common-name/cert-manager.io/subject-organizations– the CN is deprecated by RFC in favour of SANs but remains visible inopenssl x509 -textoutput and audit reports.
The dnsNames field on the generated Certificate is taken automatically from the hostname field of each TLS listener. For multiple hostnames, add more listeners or use a wildcard hostname.
5.4 Verifying issuance
| |
Within a few minutes the Certificate should reach Ready: True and the Secret referenced by certificateRefs will be populated. Envoy Gateway reloads listeners with no downtime when the Secret is updated.
6. CAA DNS record
johanneskueber.com. IN CAA 0 issue "letsencrypt.org"
johanneskueber.com. IN CAA 0 issuewild "letsencrypt.org"
johanneskueber.com. IN CAA 0 iodef "mailto:security@johanneskueber.com"
CAA declares to every public CA which authorities are authorised to issue certificates for the domain; others are required to refuse. SSL Labs reports CAA presence as a positive signal, and the record materially reduces the risk of CA-level mis-issuance. The iodef line specifies a contact for CAs that observe violations.
7. Verifying the result
Three layers of verification, in order of increasing thoroughness.
Handshake check:
| |
Cipher enumeration:
| |
Only TLSv1.2 and TLSv1.3 sections should appear, every cipher graded A, with no warnings about CBC or weak DH.
HSTS on every response, including errors:
| |
Both must return the Strict-Transport-Security header. If only the first does, HSTS is being injected per-route rather than late; switch to ClientTrafficPolicy.headers.lateResponseHeaders.
SSL Labs:
https://www.ssllabs.com/ssltest/analyze.html?d=www.johanneskueber.com
For production servers, select “Do not show the results on the boards”. The full scan takes 60-90 seconds.
8. Outlook
Topics worth looking into next:
- HTTP/3 (QUIC) via
ClientTrafficPolicy.spec.http3– performance gain on lossy networks; not graded by SSL Labs. - Dual RSA + ECDSA certificates – list two Secrets in
certificateRefswhen clients without ECDSA support must be served. - Production-grade Content Security Policy – replace the placeholder
default-src 'self'with explicit script, style, image, and connect sources, ideally with nonces or hashes. - DNSSEC – closes a DNS-level downgrade vector. Not graded by SSL Labs.
- CT log monitoring (crt.sh, Cert Spotter, Censys) – alerts on unexpected certificate issuance for the domain.
- Session ticket policy – Envoy’s defaults are safe; disable entirely if the threat model warrants it.
- Backend TLS via
BackendTLSPolicy– encrypts the Envoy-to-backend leg. Pair with anEnvoyProxyresource for backend mTLS. ListenerSet(GEP-1713) – self-service TLS for application teams on a shared Gateway. cert-manager 1.20+ supports it with the same annotations as Gateway.