Managing TLS Termination via Ingress Spec
One of the most valuable features of Ingress is its ability to handle TLS (Transport Layer Security) termination. This means the Ingress Controller can terminate the encrypted HTTPS connection from the external client, process the routing rules based on the decrypted HTTP request (host, path), and then forward the traffic to your backend Services/Pods, typically over unencrypted HTTP within the cluster network.
This significantly simplifies application development:
Your application containers don't need to handle TLS certificates or encryption/decryption.
TLS certificate management (renewal, deployment) is centralized at the Ingress layer.
Configuration for TLS termination is done directly within the Ingress
resource's spec.tls
section.
The spec.tls
Section Revisited
As briefly introduced earlier, spec.tls
is a slice of networkingv1.IngressTLS
objects. Each entry in this slice associates one or more hostnames with a Kubernetes Secret containing the necessary TLS certificate and private key.
// Go struct definition snippet
type IngressSpec struct {
// ... other fields like IngressClassName, Rules ...
TLS []IngressTLS `json:"tls,omitempty" protobuf:"bytes,3,rep,name=tls"`
}
type IngressTLS struct {
// Hosts are a list of hosts included in the TLS certificate. The values in
// this list must match the name/s used in the tlsSecret. Defaults to the
// wildcard host setting for the loadbalancer controller fulfilling this
// Ingress, if left unspecified.
Hosts []string `json:"hosts,omitempty" protobuf:"bytes,1,rep,name=hosts"`
// SecretName is the name of the secret used to terminate TLS traffic on
// port 443. Field is left optional to allow TLS routing based on SNI
// headers, if the ingress controller supports SNI.
SecretName string `json:"secretName,omitempty" protobuf:"bytes,2,opt,name=secretName"`
}
Hosts
([]string): Specifies which hostnames listed in the Ingress rules this particular TLS configuration applies to. The TLS handshake will only succeed if the hostname requested by the client is listed here (and matches the certificate's Common Name (CN) or Subject Alternative Names (SANs)). IfHosts
is omitted, the certificate inSecretName
might be used as a default/fallback certificate by some Ingress controllers.SecretName
(string): The name of the Kubernetes Secret that holds the certificate (tls.crt
) and private key (tls.key
). Crucially, this Secret MUST exist in the same namespace as the Ingress resource.
The TLS Secret (kubernetes.io/tls
)
The Secret referenced by SecretName
is not just any Secret; it must adhere to specific requirements:
Type: The Secret's
type
must bekubernetes.io/tls
.Data Keys: It must contain two specific keys in its
data
field:tls.crt
: The value must be the base64-encoded server certificate (and potentially intermediate certificates concatenated).tls.key
: The value must be the base64-encoded private key corresponding to the certificate.
You typically create such Secrets using kubectl create secret tls <secret-name> --cert=/path/to/cert.pem --key=/path/to/key.pem -n <namespace>
or by defining them in YAML.
Adding TLS Configuration Programmatically
Adding or modifying the spec.tls
section in your Go code follows the same principles as managing rules. You construct or modify the []networkingv1.IngressTLS
slice within your Ingress
struct before calling Create
or Update
.
Let's adapt the update example from the previous section. Suppose we want to ensure the app.example.com
host in our example-ingress
uses the TLS secret app-tls-secret
.
// --- Snippet within the RetryOnConflict update function ---
// Step 2: Modify the retrieved object in memory
// ... (previous code to potentially add paths) ...
// Ensure TLS configuration exists for the target host
tlsHost := "app.example.com"
tlsSecretName := "app-tls-secret"
tlsEntryExists := false
tlsNeedsUpdate := false // Flag to track if we modify the TLS section
// Check if a TLS entry for the secret already exists
for i, tlsEntry := range currentIngress.Spec.TLS {
if tlsEntry.SecretName == tlsSecretName {
tlsEntryExists = true
// Check if the host is already listed for this secret
hostAlreadyListed := false
for _, host := range tlsEntry.Hosts {
if host == tlsHost {
hostAlreadyListed = true
break
}
}
// If host is not listed, add it to the existing entry
if !hostAlreadyListed {
log.Printf("Host '%s' not found in existing TLS entry for secret '%s'. Adding it.\n", tlsHost, tlsSecretName)
currentIngress.Spec.TLS[i].Hosts = append(currentIngress.Spec.TLS[i].Hosts, tlsHost)
tlsNeedsUpdate = true
} else {
log.Printf("Host '%s' already listed for TLS secret '%s'.\n", tlsHost, tlsSecretName)
}
break // Found the relevant secret entry
}
}
// If no entry exists for this secret at all, add a new one
if !tlsEntryExists {
log.Printf("No existing TLS entry found for secret '%s'. Adding new entry for host '%s'.\n", tlsSecretName, tlsHost)
newTlsEntry := networkingv1.IngressTLS{
Hosts: []string{tlsHost},
SecretName: tlsSecretName,
}
currentIngress.Spec.TLS = append(currentIngress.Spec.TLS, newTlsEntry)
tlsNeedsUpdate = true
}
// Step 3: Call Update only if changes were actually made
if ruleUpdated || tlsNeedsUpdate { // Check if either rules or TLS were modified
log.Printf("Attempting to update Ingress '%s' (ResourceVersion: %s) due to rule/TLS changes...\n", currentIngress.Name, currentIngress.ResourceVersion)
_, updateErr := ingressClient.Update(context.TODO(), currentIngress, metav1.UpdateOptions{})
if updateErr == nil {
log.Println("Update successful!")
} else {
log.Printf("Update failed: %v. Retrying...\n", updateErr)
}
return updateErr // Return error for RetryOnConflict
} else {
log.Println("No modifications needed for rules or TLS. Skipping update call.")
return nil // No update needed, stop retrying
}
// --- End Snippet ---
Explanation:
Find/Add Logic: We iterate through the existing
spec.TLS
slice.Check Secret: We look for an entry matching the desired
SecretName
.Check Host: If the secret entry exists, we check if our target
Host
is already listed. If not, we append it to theHosts
slice of that entry.Add New Entry: If no entry for the
SecretName
exists, we create a newnetworkingv1.IngressTLS
struct and append it to thespec.TLS
slice.Conditional Update: We introduce flags (
ruleUpdated
,tlsNeedsUpdate
) to track if we actually made any changes. We only callingressClient.Update
if a modification occurred, preventing unnecessary API calls and potential conflicts.
Prerequisites and Considerations:
Secret Existence: The referenced Secret (
app-tls-secret
) must exist in the same namespace before the Ingress Controller tries to use it. Creating the Ingress resource itself doesn't create the Secret.Secret Permissions: The Ingress Controller's Service Account needs RBAC permissions (usually
get
,list
,watch
) for Secrets in the namespaces it manages Ingress resources for.Certificate Management: Directly managing TLS Secrets via
kubectl
orclient-go
is feasible for a few certificates, but it quickly becomes complex to handle renewals. Tools like cert-manager are commonly used in Kubernetes to automate the issuance and renewal of TLS certificates (e.g., from Let's Encrypt) and automatically create/update the necessarykubernetes.io/tls
Secrets. Your Go code would then just reference theSecretName
managed by cert-manager.
Programmatically managing the spec.tls
section allows you to automate the configuration of secure external access for your applications, ensuring that the correct certificates are associated with the right hostnames as defined in your Ingress rules.
Last updated
Was this helpful?