Enable HTTPS on the Kestra UI with SSL/TLS certificates

For the complete documentation index, see llms.txt. For a full content snapshot, see llms-full.txt. Append .md to any kestra.io/docs/* URL for plain Markdown.

Configure secure access to the Kestra UI via HTTPS. The right approach depends on your deployment type.

Using Caddy as a reverse proxy

Caddy is a reverse proxy that manages HTTPS automatically. For public domains it obtains and renews Let’s Encrypt certificates without any manual steps; for local development it generates a local CA and prompts once for system trust.

Local development (macOS)

This setup gives multiple local Kestra instances clean HTTPS URLs (e.g. https://oss, https://ee) with a single local CA prompt.

  1. Install Caddy:

    brew install caddy
  2. Create a ~/Caddyfile with one reverse_proxy block per Kestra instance (adjust ports to match your instances):

    oss {
    reverse_proxy localhost:8080
    }
    oss-dev {
    reverse_proxy localhost:8081
    }
    ee {
    reverse_proxy localhost:8082
    }
    ee-dev {
    reverse_proxy localhost:8083
    }
  3. Run Caddy:

    sudo caddy run --config ~/Caddyfile

    Caddy generates a local CA on first run and prompts once to trust it in the macOS Keychain. After that, all configured hostnames are available over HTTPS with no browser warnings.

Production (public domain)

For a publicly accessible Kestra instance, point your domain at the server and Caddy handles certificate issuance and renewal automatically via Let’s Encrypt.

kestra.yourdomain.com {
reverse_proxy localhost:8080
}

Run Caddy as a background service so it persists across reboots:

sudo caddy start --config /etc/caddy/Caddyfile

With this setup, Kestra itself runs on plain HTTP internally (localhost:8080) and Caddy terminates TLS at the edge — no changes to Kestra’s Micronaut SSL configuration are needed.

Creating self-signed certificates

To get started in lower environments, create self-signed certificates using the OpenSSL library. For more detail on examining certificates and keys, see this Micronaut article.

## Create a folder which will be later mounted to the kestra container
mkdir -p /app/ssl
cd /app/ssl
## Create CA in PEM format along with private key
openssl req -x509 -sha256 -days 365 -newkey rsa:4096 \
-keyout cacert.key -out cacert.pem \
-subj '/CN=example.kestra.com/C=IE/O=kestra' \
-passout pass:changeit
## Create certificate signing request
openssl req -newkey rsa:4096 \
-keyout server.key -out server.csr \
-subj '/CN=example.kestra.com/C=IE/O=kestra' \
-passout pass:changeit
## Create the server configuration which will be used to sign the certificate
cat <<< 'authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost' > server.conf
## sign certificate
openssl x509 -req -CA cacert.pem -CAkey cacert.key \
-in server.csr -out server.pem -days 365 \
-CAcreateserial -extfile server.conf \
-passin pass:changeit
## Create server.p12
openssl pkcs12 -export -out server.p12 -name "localhost" \
-inkey server.key -in server.pem \
-passin pass:changeit \
-passout pass:changeit
## Create keystore.p12 with JDK keytool
keytool -importkeystore -srckeystore server.p12 \
-srcstoretype pkcs12 -destkeystore keystore.p12 \
-deststoretype pkcs12 \
-deststorepass changeit -srcstorepass changeit
## Create truststore.jks
keytool -import -trustcacerts -noprompt -alias ca \
-ext san=dns:localhost,ip:127.0.0.1 \
-file cacert.pem -keystore truststore.jks \
-storepass changeit -keypass changeit

Micronaut SSL configuration

Configure HTTPS through the micronaut settings in the Observability and Networking configuration.

kestra:
image: registry.kestra.io/docker/kestra:latest
pull_policy: always
user: "root"
command: server standalone --worker-thread=128
volumes:
- kestra-data:/app/storage
- /var/run/docker.sock:/var/run/docker.sock
- tmp-kestra:/tmp/kestra-wd
- /app/ssl:/app/ssl
ports:
- "8443:8443"
environment:
KESTRA_CONFIGURATION: |
micronaut:
security:
x509:
enabled: false
ssl:
enabled: true
server:
ssl:
port: 8443
enabled: true
clientAuthentication: want
keyStore:
path: file:/app/ssl/server.p12
password: changeit
type: PKCS12
trustStore:
path: file:/app/ssl/truststore.jks
password: changeit
type: JKS
datasources:
postgres:
url: jdbc:postgresql://postgres:5432/kestra
driver-class-name: org.postgresql.Driver
username: kestra
password: k3str4
kestra:
server:
basic-auth:
enabled: false
username: "admin@kestra.io" # it must be a valid email address
password: kestra
repository:
type: postgres
storage:
type: local
local:
base-path: "/app/storage"
queue:
type: postgres
tasks:
tmp-dir:
path: /tmp/kestra-wd/tmp
ports:
- "8443:8443"

Outbound SSL configuration

If Kestra tasks make outbound calls to services that require SSL, configure the JVM to trust your certificates by setting the following options in the JAVA_OPTS environment variable in your Observability and Networking configuration:

JAVA_OPTS: "-Djavax.net.ssl.trustStore=/app/ssl/truststore.jks -Djavax.net.ssl.trustStorePassword=changeit"

The following example shows the full service configuration with JAVA_OPTS set:

kestra:
image: registry.kestra.io/docker/kestra:latest
pull_policy: always
user: "root"
command: server standalone --worker-thread=128
volumes:
- kestra-data:/app/storage
- /var/run/docker.sock:/var/run/docker.sock
- tmp-kestra:/tmp/kestra-wd
- /app/ssl:/app/ssl
ports:
- "8443:8443"
environment:
JAVA_OPTS: "-Djavax.net.ssl.trustStore=/app/ssl/truststore.jks -Djavax.net.ssl.trustStorePassword=changeit"
KESTRA_CONFIGURATION: |
micronaut:
security:
x509:
enabled: false
ssl:
enabled: true
server:
ssl:
port: 8443
enabled: true
clientAuthentication: want
keyStore:
path: file:/app/ssl/server.p12
password: changeit
type: PKCS12
trustStore:
path: file:/app/ssl/truststore.jks
password: changeit
type: JKS
datasources:
postgres:
url: jdbc:postgresql://postgres:5432/kestra
driver-class-name: org.postgresql.Driver
username: kestra
password: k3str4
kestra:
server:
basic-auth:
enabled: false
username: "admin@kestra.io" # it must be a valid email address
password: kestra
repository:
type: postgres
storage:
type: local
local:
base-path: "/app/storage"
queue:
type: postgres
tasks:
tmp-dir:
path: /tmp/kestra-wd/tmp
ports:
- "8443:8443"

CSRF protection

Cross-site request forgery (CSRF) is an attack where a malicious website tricks a signed-in user’s browser into sending requests to Kestra.

Kestra 2.0 ships CSRF protection enabled by default. It uses a double-submit token: the webserver sets a csrfToken cookie and injects the same value into the UI, which sends it back in the X-CSRF-TOKEN header on every state-changing request. The token is validated only when the request is authenticated by a session cookie (BASIC_AUTH or JWT). API and SDK clients that authenticate with an Authorization header are not affected, and GET, HEAD, and OPTIONS requests are never checked.

CSRF protection does not require TLS. The Secure flag on the token cookie follows the request scheme, so plain-HTTP deployments keep working. The token is signed with kestra.encryption.secret-key when one is configured.

To disable it, for example while debugging a reverse proxy setup, set:

micronaut:
security:
csrf:
enabled: false

Configuring SSL with Kubernetes

Kubernetes deployments support HTTPS via ingress-level TLS termination or via self-signed certificates at the application level.

Most cloud providers expect TLS termination at the ingress controller. The following steps configure HTTPS using Let’s Encrypt certificates:

  1. Install cert-manager. To use a different version, see available releases on GitHub:

    kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.1/cert-manager.yaml
  2. Create a Let’s Encrypt issuer (replace your-email@example.com):

    apiVersion: cert-manager.io/v1
    kind: ClusterIssuer
    metadata:
    name: letsencrypt-prod
    spec:
    acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your-email@example.com
    privateKeySecretRef:
    name: letsencrypt-prod
    solvers:
    - http01:
    ingress:
    class: nginx # Update for your ingress controller
  3. Configure Ingress with TLS (Azure AKS example):

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
    name: kestra-ingress
    annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
    spec:
    tls:
    - hosts:
    - kestra.yourdomain.com
    secretName: kestra-tls
    rules:
    - host: kestra.yourdomain.com
    http:
    paths:
    - path: /
    pathType: Prefix
    backend:
    service:
    name: kestra-service
    port:
    number: 80

Using self-signed certificates (for testing)

  1. Generate certificates using the OpenSSL commands in the Creating self-signed certificates section above.

  2. Create TLS secret:

    kubectl create secret tls kestra-tls \
    --cert=server.pem \
    --key=server.key
  3. Reference the secret in your Ingress:

    spec:
    tls:
    - hosts:
    - kestra.yourdomain.com
    secretName: kestra-tls

Application-level SSL configuration

For environments where ingress TLS termination isn’t available:

  1. Create secret with SSL files:

    kubectl create secret generic kestra-ssl \
    --from-file=keystore.p12 \
    --from-file=truststore.jks
  2. Configure Kestra deployment:

    env:
    - name: KESTRA_CONFIGURATION
    value: |
    micronaut:
    server:
    ssl:
    enabled: true
    port: 8443
    keyStore:
    path: file:/app/ssl/keystore.p12
    password: changeit
    type: PKCS12
    volumeMounts:
    - name: ssl-secret
    mountPath: "/app/ssl"
    volumes:
    - name: ssl-secret
    secret:
    secretName: kestra-ssl
  3. Expose HTTPS port in your service:

    ports:
    - name: https
    port: 8443
    targetPort: 8443

Verifying the configuration

Certificate validity can be checked with:

kubectl get certificate kestra-tls -w

Expected output:

NAME READY SECRET AGE
kestra-tls True kestra-tls 5m

For a step-by-step guide to adding a self-signed or internal CA to the JVM truststore in a Kubernetes deployment, see Trusting a custom CA for outbound connections on Kubernetes.

Was this page helpful?