Stage 7 · Master
Phase 19 — Production Deployment
Ingress
Route external traffic through exactly one Ingress to gateway's Service, issue TLS automatically with cert-manager, and handle the one legitimate exception — an unauthenticated payment-webhook path — without weakening the rule for every other service.
Only gateway's Service Should Ever Appear in an Ingress Rule
API Gateway's deployment lesson already established that gateway is the only public entry point, and enforced it with a NetworkPolicy. This lesson moves that rule up one layer, to the cluster edge, where it is far easier to violate: adding an Ingress rule takes one YAML block, and nothing about that block hints that it bypasses authentication, rate limiting, and tenant-header stripping. The rule to encode is one sentence long — exactly one Service is ever an Ingress backend — and the audit command at the end of this lesson exists because sentences do not enforce themselves.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gateway
namespace: {{ .Release.Namespace }}
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/proxy-body-size: "2m"
spec:
ingressClassName: nginx
tls:
- hosts: [{{ .Values.hostname | quote }}]
secretName: gateway-tls
rules:
- host: {{ .Values.hostname | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: gateway, port: { name: http } }cert-manager.io/cluster-issuer references a ClusterIssuer already configured cluster-wide (below); the annotation is all this specific Ingress needs to obtain and continuously renew a real TLS certificate for {{ .Values.hostname }}.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: platform-team@hoa-platform.example
privateKeySecretRef: { name: letsencrypt-production-account-key }
solvers:
- http01:
ingress: { ingressClassName: nginx }A ClusterIssuer is cluster-scoped by design — every namespace's Ingress resources reference the same one, so certificate issuance policy is configured exactly once rather than duplicated per service chart.
A Payment Webhook Cannot Authenticate the Way gateway Requires — and Should Not Have To
services/payment-service/internal/payment receives asynchronous webhook callbacks from an external payment processor confirming that a charge settled. The processor's webhook request carries its own HMAC signature header, not a JWT gateway's auth middleware understands — routing it through gateway would mean either teaching gateway to special-case one payment provider's signature scheme, or rejecting every webhook outright. Exposing payment's webhook receiver directly, on its own narrowly scoped Ingress path, is the correct exception — provided it does not become the template for exposing anything else directly.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: payment-webhook
namespace: {{ .Release.Namespace }}
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/limit-rps: "20" # far below gateway's own aggregate rate limit — this path has one known caller
nginx.ingress.kubernetes.io/whitelist-source-range: {{ .Values.paymentProviderIpRanges | join "," | quote }}
spec:
ingressClassName: nginx
tls:
- hosts: [{{ .Values.webhookHostname | quote }}]
secretName: payment-webhook-tls
rules:
- host: {{ .Values.webhookHostname | quote }}
http:
paths:
- path: /webhooks/payment-processor
pathType: Exact
backend:
service: { name: payment, port: { name: http } }pathType: Exact (not Prefix) means this rule matches only this one literal path — no other route on payment's Service becomes accidentally reachable through this same Ingress if payment ever adds more HTTP routes later.
Skipping gateway's authentication is a deliberate, narrow exception — it does not remove the need for the request to be verified some other way. services/payment-service/internal/payment's webhook handler independently verifies the processor's HMAC signature over the raw request body on every request; a request that reaches this Ingress path without a valid signature is rejected by the handler itself, not by anything upstream.
func (h *WebhookHandler) HandlePaymentProcessorCallback(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 2<<20)) // matches the Ingress's proxy-body-size: 2m limit
if err != nil {
http.Error(w, "unreadable body", http.StatusBadRequest)
return
}
signature := r.Header.Get("X-Provider-Signature")
if !hmac.Equal([]byte(signature), h.expectedSignature(body)) {
// Deliberately generic response: neither confirms nor denies which part
// of verification failed, to avoid helping an attacker iterate.
http.Error(w, "invalid request", http.StatusUnauthorized)
return
}
// ... process the verified webhook payload
}This handler is the actual authentication boundary for this one path — the Ingress's whitelist-source-range and rate limit are defense-in-depth, not the primary control, exactly because this path was deliberately carved out of gateway's normal enforcement.
Prove Both That Routing Works and That the Boundary Was Not Accidentally Widened
Applied exercise
Audit the cluster for an accidentally exposed backend service
A teammate's draft PR adds an Ingress rule directly for services/notice-service/internal/notice's Service, intending to let a partner integration fetch notice-board content without going through gateway.
- Explain in writing why this request should be routed through gateway instead, referencing the specific protections it would bypass.
- Propose the correct alternative: either a new authenticated route behind gateway, or a payment-webhook-style narrow exception with its own independent verification.
- If a narrow exception is genuinely justified, specify what independent verification (matching the payment webhook's HMAC check) the notice service's handler would need.
- Run the cluster-wide Ingress audit command from this lesson and confirm no unauthorized Ingress objects exist after your review.
Deliverable
A written review rejecting or reshaping the draft PR, a concrete alternative design, and a confirmed clean Ingress audit.
Completion checks
- The review explicitly names the gateway protections (auth, rate limiting) a direct Ingress would bypass.
- Any proposed exception includes its own independent verification mechanism, not just network-level restrictions.
- The final Ingress audit shows exactly the expected set of objects, with nothing extra.
Why does the payment-webhook Ingress rule use pathType: Exact for /webhooks/payment-processor instead of pathType: Prefix?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.