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)). If Hosts is omitted, the certificate in SecretName 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:

  1. Type: The Secret's type must be kubernetes.io/tls.

  2. 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:

  1. Find/Add Logic: We iterate through the existing spec.TLS slice.

  2. Check Secret: We look for an entry matching the desired SecretName.

  3. Check Host: If the secret entry exists, we check if our target Host is already listed. If not, we append it to the Hosts slice of that entry.

  4. Add New Entry: If no entry for the SecretName exists, we create a new networkingv1.IngressTLS struct and append it to the spec.TLS slice.

  5. Conditional Update: We introduce flags (ruleUpdated, tlsNeedsUpdate) to track if we actually made any changes. We only call ingressClient.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 or client-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 necessary kubernetes.io/tls Secrets. Your Go code would then just reference the SecretName 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?