Secure communication

Lightning AI deployments support end-to-end TLS encryption, a critical feature for achieving a SOC 2 compliant production environment.

When you enable this feature, the Lightning Platform passes the necessary TLS certificate and private key to your container as the LIGHTNING_CERT_PEM and LIGHTNING_KEY_FILE environment variables.

Your application is responsible for loading these certificates to activate secure (HTTPS) communication.

Python FastAPI example

Below is a reference implementation for a Python FastAPI server.

1/3
2/3
3/3
# Standard library imports for handling environment variables, files, and data encoding
import base64
import os
import tempfile
from pathlib import Path
from typing import Any, Dict

# Third-party imports for the web server
import uvicorn
from fastapi import FastAPI


def load_ssl_context() -> Dict[str, Any]:
    """
    Checks for TLS certificate environment variables and prepares them for Uvicorn.

    If `LIGHTNING_CERT_PEM` and `LIGHTNING_KEY_FILE` are found, this function
    decodes them from base64, writes them to temporary files, and returns a
    dictionary with paths for Uvicorn to use. Otherwise, it returns an empty dict.
    """
    cert_pem_b64 = os.getenv("LIGHTNING_CERT_PEM")
    key_pem_b64 = os.getenv("LIGHTNING_KEY_FILE")

    # Proceed only if both environment variables are set.
    if cert_pem_b64 and key_pem_b64:
        try:
            # Decode the base64 strings to get the actual PEM content.
            cert_pem = base64.b64decode(cert_pem_b64).decode('utf-8')
            key_pem = base64.b64decode(key_pem_b64).decode('utf-8')

            # Create temporary files to hold the certificate and key.
            # IMPORTANT: `delete=False` means these files will NOT be automatically
            # cleaned up when the server stops. This is required by this specific
            # logic because Uvicorn needs to read the files after this function
            # returns. In a production system, you should consider a cleanup
            # strategy for these files upon server shutdown.
            with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix=".pem") as cert_file, \
                 tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix=".pem") as key_file:

                cert_file.write(cert_pem)
                key_file.write(key_pem)

                # Return a dictionary with Path objects for Uvicorn's SSL arguments.
                print("TLS certificates found. Starting server with HTTPS.")
                return {
                    "ssl_keyfile": Path(key_file.name),
                    "ssl_certfile": Path(cert_file.name)
                }

        except (base64.binascii.Error, IOError) as e:
            # If there's an error decoding or writing, fall back to HTTP.
            print(f"WARNING: Error processing certificates: {e}. Starting without HTTPS.")

    # If variables are not set or an error occurred, return an empty dictionary.
    print("TLS certificates not found. Starting server with HTTP.")
    return {}


app = FastAPI(
    title="Secure FastAPI Server",
    description="An example server that can be run with or without TLS."
)

@app.get("/")
def read_root():
    """A simple endpoint to confirm the server is running."""
    return {"message": "Hello from your FastAPI server!"}


if __name__ == "__main__":
    # The `load_ssl_context()` function returns a dictionary.
    # The `**` operator unpacks this dictionary into keyword arguments for `uvicorn.run`.
    # - If certs are found, it's like calling:
    #   run(app, ssl_keyfile=Path(...), ssl_certfile=Path(...))
    # - If not found, it's like calling:
    #   run(app)
    ssl_config = load_ssl_context()
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000,
        **ssl_config
    )

Note: If you are using LitServe, this is done automatically for you.

Go example

This code is a concise, runnable example that demonstrates the core task. It's heavily commented to explain each step, especially the handling of base64-encoded certificate data.

package main

# [Imports]
import (
	"crypto/tls"
	"encoding/base64"
	"fmt"
	"log"
	"net/http"
	"time"
)

# [Prepare SSL Context]
# createTLSConfig prepares the TLS configuration from base64-encoded PEM data.
# In a real application, these strings would come from environment variables.
func createTLSConfig() *tls.Config {
	# Assume these base64-encoded variables are passed to your application.
	# For this example, they are hardcoded. In production, you would use:
	certPEM_b64 := os.Getenv("LIGHTNING_CERT_PEM")
	keyPEM_b64  := os.Getenv("LIGHTNING_KEY_FILE")

	# The logic will only proceed if both variables are non-empty.
	if certPEM_b64 == "" || keyPEM_b64 == "" {
		return nil # No certificates provided, so no TLS config.
	}

	# Decode the base64 strings to get the raw PEM data.
	certPEM, err := base64.StdEncoding.DecodeString(certPEM_b64)
	if err != nil {
		log.Printf("WARNING: Failed to decode certificate, starting without TLS: %v", err)
		return nil
	}
	keyPEM, err := base64.StdEncoding.DecodeString(keyPEM_b64)
	if err != nil {
		log.Printf("WARNING: Failed to decode private key, starting without TLS: %v", err)
		return nil
	}

	# Create a TLS certificate object directly from the PEM data.
	cert, err := tls.X509KeyPair(certPEM, keyPEM)
	if err != nil {
		log.Printf("WARNING: Failed to load TLS key pair, starting without TLS: %v", err)
		return nil
	}

	# Return a TLS configuration containing the certificate.
	return &tls.Config{
		Certificates: []tls.Certificate{cert},
		MinVersion:   tls.VersionTLS12,
	}
}

# [Create and run Server]
func main() {
	# Define a simple handler for the root path.
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello from your Go server!")
	})

	# Prepare the server configuration.
	server := &http.Server{
		Addr:              ":8080",
		ReadHeaderTimeout: 3 * time.Second,
		# Get the TLS configuration by calling our function.
		TLSConfig:         createTLSConfig(),
	}

	# Conditionally start the server with or without TLS.
	if server.TLSConfig != nil {
		# If TLSConfig is not nil, start an HTTPS server.
		# The certFile and keyFile arguments are empty because the certificates
		# are already loaded in the server's TLSConfig.
		log.Println("TLS configuration found. Starting server with HTTPS on port 8080.")
		if err := server.ListenAndServeTLS("", ""); err != nil {
			log.Fatalf("FATAL: HTTPS server failed: %v", err)
		}
	} else {
		# If TLSConfig is nil, start a standard HTTP server.
		log.Println("TLS configuration not found. Starting server with HTTP on port 8080.")
		if err := server.ListenAndServe(); err != nil {
			log.Fatalf("FATAL: HTTP server failed: %v", err)
		}
	}
}

Was this page helpful?

What went wrong?

↑↓ Navigate↵ Open⌘↵ New tabesc Close