feat(tls): add custom tls certificate support for corporate deployments

adds caddy-addon mechanism for custom certificates when let's encrypt
is not available. includes setup script with interactive wizard,
example configs, and documentation.
This commit is contained in:
Yury Kossakovsky
2026-01-09 23:26:41 -07:00
parent e8f7934224
commit 0e4b46ec31
10 changed files with 590 additions and 6 deletions

10
.gitignore vendored
View File

@@ -11,4 +11,12 @@ dify/
volumes/
docker-compose.override.yml
docker-compose.n8n-workers.yml
welcome/data.json
welcome/data.json
# Custom TLS certificates
certs/*
!certs/.gitkeep
# Custom Caddy addons (user configurations)
caddy-addon/*.conf
!caddy-addon/*.example

View File

@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.1.0] - 2026-01-09
### Added
- **Custom TLS certificates** - Support for corporate/internal certificates via `caddy-addon/` mechanism
- New `make setup-tls` command and `scripts/setup_custom_tls.sh` helper script for easy certificate configuration
- New `make git-pull` command for fork workflows - merges from upstream instead of hard reset
## [1.0.0] - 2026-01-07
### Added

View File

@@ -79,6 +79,7 @@ make show-restarts # Show restart count per container
make doctor # Run system diagnostics (DNS, SSL, containers, disk, memory)
make import # Import n8n workflows from backup
make import n=10 # Import first N workflows only
make setup-tls # Configure custom TLS certificates
make switch-beta # Switch to develop branch and update
make switch-stable # Switch to main branch and update

View File

@@ -1,4 +1,4 @@
.PHONY: help install update update-preview git-pull clean clean-all logs status monitor restart show-restarts doctor switch-beta switch-stable import
.PHONY: help install update update-preview git-pull clean clean-all logs status monitor restart show-restarts doctor switch-beta switch-stable import setup-tls
PROJECT_NAME := localai
@@ -21,6 +21,7 @@ help:
@echo " make doctor Run system diagnostics"
@echo " make import Import n8n workflows from backup"
@echo " make import n=10 Import first N workflows only"
@echo " make setup-tls Configure custom TLS certificates"
@echo ""
@echo " make switch-beta Switch to beta (develop branch)"
@echo " make switch-stable Switch to stable (main branch)"
@@ -88,3 +89,6 @@ ifdef n
else
docker compose -p $(PROJECT_NAME) run --rm -e FORCE_IMPORT=true n8n-import
endif
setup-tls:
bash ./scripts/setup_custom_tls.sh

View File

@@ -322,11 +322,12 @@ The project includes a Makefile for simplified command execution:
| `make import` | Import n8n workflows from backup |
| `make import n=10` | Import first N workflows only |
### Diagnostics
### Diagnostics & Configuration
| Command | Description |
| ------------- | ------------------------------------------------------------------ |
| `make doctor` | Run system diagnostics (checks DNS, SSL, containers, disk, memory) |
| Command | Description |
| ---------------- | ------------------------------------------------------------------ |
| `make doctor` | Run system diagnostics (checks DNS, SSL, containers, disk, memory) |
| `make setup-tls` | Configure custom TLS certificates for corporate/internal use |
Run `make help` for the full list of available commands.

97
caddy-addon/README.md Normal file
View File

@@ -0,0 +1,97 @@
# Caddy Addons
This directory allows you to extend or override Caddy configuration without modifying the main `Caddyfile`.
All `.conf` files in this directory are automatically imported via `import /etc/caddy/addons/*.conf` at the end of the main Caddyfile.
## Use Cases
- Custom TLS certificates (corporate/internal CA)
- Additional reverse proxy rules
- Custom headers or middleware
- Rate limiting or access control
## Custom TLS Certificates
For corporate/internal deployments where Let's Encrypt is not available, you can use your own certificates.
### Quick Setup
1. Place your certificates in the `certs/` directory:
```bash
cp /path/to/your/cert.crt ./certs/wildcard.crt
cp /path/to/your/key.key ./certs/wildcard.key
```
2. Run the setup script:
```bash
make setup-tls
```
3. Restart Caddy:
```bash
docker compose -p localai restart caddy
```
### Manual Setup
1. Copy the example file:
```bash
cp caddy-addon/custom-tls.conf.example caddy-addon/custom-tls.conf
```
2. Edit `custom-tls.conf` with your hostnames and certificate paths
3. Place certificates in `certs/` directory
4. Restart Caddy:
```bash
docker compose -p localai restart caddy
```
## How Site Override Works
When you define a site block in an addon file with the same hostname as the main Caddyfile, Caddy will use **both** configurations. To completely override a site, use the exact same hostname.
Example: To override `n8n.yourdomain.com` with a custom certificate:
```
# caddy-addon/custom-tls.conf
n8n.internal.company.com {
tls /etc/caddy/certs/wildcard.crt /etc/caddy/certs/wildcard.key
reverse_proxy n8n:5678
}
```
Make sure your `.env` file has `N8N_HOSTNAME=n8n.internal.company.com`.
## File Structure
```
caddy-addon/
├── .gitkeep # Keeps directory in git
├── README.md # This file
├── custom-tls.conf.example # Example for custom certificates
└── custom-tls.conf # Your custom config (gitignored)
certs/
├── .gitkeep # Keeps directory in git
├── wildcard.crt # Your certificate (gitignored)
└── wildcard.key # Your private key (gitignored)
```
## Important Notes
- Files in `caddy-addon/*.conf` are gitignored (preserved during updates)
- Files in `certs/` are gitignored (certificates are not committed)
- Example files (`*.example`) are tracked in git
- Caddy validates configuration on startup - check logs if it fails:
```bash
docker compose -p localai logs caddy
```
## Caddy Documentation
- [Caddyfile Syntax](https://caddyserver.com/docs/caddyfile)
- [TLS Directive](https://caddyserver.com/docs/caddyfile/directives/tls)
- [Reverse Proxy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)

View File

@@ -0,0 +1,114 @@
# Custom TLS Configuration for Corporate/Internal Certificates
#
# This file provides examples for using your own TLS certificates instead of Let's Encrypt.
# Copy this file to custom-tls.conf and modify as needed.
#
# Prerequisites:
# 1. Place your certificate files in the ./certs/ directory
# 2. Update .env hostnames to match your internal domain
# 3. Restart Caddy: docker compose -p localai restart caddy
# =============================================================================
# Option 1: Reusable TLS snippet (recommended for wildcard certificates)
# =============================================================================
# Define once, import in each service block
(custom_tls) {
tls /etc/caddy/certs/wildcard.crt /etc/caddy/certs/wildcard.key
}
# Then for each service you want to override:
#
# n8n.internal.company.com {
# import custom_tls
# reverse_proxy n8n:5678
# }
#
# flowise.internal.company.com {
# import custom_tls
# reverse_proxy flowise:3001
# }
# =============================================================================
# Option 2: Individual service configuration
# =============================================================================
# Use when you have different certificates for different services
# n8n.internal.company.com {
# tls /etc/caddy/certs/n8n.crt /etc/caddy/certs/n8n.key
# reverse_proxy n8n:5678
# }
# =============================================================================
# Option 3: Internal CA with auto-reload
# =============================================================================
# Caddy can auto-reload certificates when they change
# n8n.internal.company.com {
# tls /etc/caddy/certs/cert.pem /etc/caddy/certs/key.pem {
# # Optional: specify CA certificate for client verification
# # client_auth {
# # mode require_and_verify
# # trusted_ca_cert_file /etc/caddy/certs/ca.pem
# # }
# }
# reverse_proxy n8n:5678
# }
# =============================================================================
# Full Example: All common services with wildcard certificate
# =============================================================================
# Uncomment and modify the hostnames to match your .env configuration
# # N8N
# n8n.internal.company.com {
# import custom_tls
# reverse_proxy n8n:5678
# }
# # Flowise
# flowise.internal.company.com {
# import custom_tls
# reverse_proxy flowise:3001
# }
# # Open WebUI
# webui.internal.company.com {
# import custom_tls
# reverse_proxy open-webui:8080
# }
# # Grafana
# grafana.internal.company.com {
# import custom_tls
# reverse_proxy grafana:3000
# }
# # Portainer
# portainer.internal.company.com {
# import custom_tls
# reverse_proxy portainer:9000
# }
# # Langfuse
# langfuse.internal.company.com {
# import custom_tls
# reverse_proxy langfuse-web:3000
# }
# # Supabase
# supabase.internal.company.com {
# import custom_tls
# reverse_proxy kong:8000
# }
# # Welcome Page (with basic auth preserved)
# welcome.internal.company.com {
# import custom_tls
# basic_auth {
# {$WELCOME_USERNAME} {$WELCOME_PASSWORD_HASH}
# }
# root * /srv/welcome
# file_server
# try_files {path} /index.html
# }

0
certs/.gitkeep Normal file
View File

View File

@@ -309,6 +309,7 @@ services:
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-addon:/etc/caddy/addons:ro
- ./certs:/etc/caddy/certs:ro
- ./welcome:/srv/welcome:ro
- caddy-data:/data:rw
- caddy-config:/config:rw

351
scripts/setup_custom_tls.sh Executable file
View File

@@ -0,0 +1,351 @@
#!/usr/bin/env bash
# =============================================================================
# setup_custom_tls.sh - Configure custom TLS certificates for Caddy
# =============================================================================
# Generates caddy-addon/custom-tls.conf for using corporate/internal certificates
# instead of Let's Encrypt.
#
# Usage:
# bash scripts/setup_custom_tls.sh # Interactive mode
# bash scripts/setup_custom_tls.sh cert.crt key.key # Non-interactive mode
#
# Prerequisites:
# - Place certificate files in ./certs/ directory
# - Certificate paths are relative to container (/etc/caddy/certs/)
# =============================================================================
set -euo pipefail
source "$(dirname "$0")/utils.sh" && init_paths
ADDON_FILE="$PROJECT_ROOT/caddy-addon/custom-tls.conf"
CERTS_DIR="$PROJECT_ROOT/certs"
# =============================================================================
# FUNCTIONS
# =============================================================================
show_help() {
cat << EOF
Setup Custom TLS Certificates for Caddy
Usage: $(basename "$0") [OPTIONS] [CERT_FILE] [KEY_FILE]
Options:
-h, --help Show this help message
--remove Remove custom TLS configuration
Arguments:
CERT_FILE Path to certificate file (relative to ./certs/)
KEY_FILE Path to private key file (relative to ./certs/)
Examples:
$(basename "$0") # Interactive mode
$(basename "$0") wildcard.crt wildcard.key # Use specific files
$(basename "$0") --remove # Remove custom TLS config
The script will:
1. Detect certificate files in ./certs/
2. Read active services from .env
3. Generate caddy-addon/custom-tls.conf
4. Optionally restart Caddy
EOF
}
find_certificates() {
# Find certificate files in certs directory
local certs=()
if [[ -d "$CERTS_DIR" ]]; then
while IFS= read -r -d '' file; do
certs+=("$(basename "$file")")
done < <(find "$CERTS_DIR" -maxdepth 1 -type f \( -name "*.crt" -o -name "*.pem" -o -name "*.cer" \) -print0 2>/dev/null)
fi
echo "${certs[*]:-}"
}
find_keys() {
# Find key files in certs directory
local keys=()
if [[ -d "$CERTS_DIR" ]]; then
while IFS= read -r -d '' file; do
keys+=("$(basename "$file")")
done < <(find "$CERTS_DIR" -maxdepth 1 -type f \( -name "*.key" -o -name "*-key.pem" \) -print0 2>/dev/null)
fi
echo "${keys[*]:-}"
}
get_active_services() {
# Get list of services with their hostnames from .env
load_env
local services=()
# Map of service names to their hostname variables
declare -A service_map=(
["n8n"]="N8N_HOSTNAME"
["flowise"]="FLOWISE_HOSTNAME"
["webui"]="WEBUI_HOSTNAME"
["grafana"]="GRAFANA_HOSTNAME"
["prometheus"]="PROMETHEUS_HOSTNAME"
["portainer"]="PORTAINER_HOSTNAME"
["langfuse"]="LANGFUSE_HOSTNAME"
["supabase"]="SUPABASE_HOSTNAME"
["dify"]="DIFY_HOSTNAME"
["nocodb"]="NOCODB_HOSTNAME"
["ragapp"]="RAGAPP_HOSTNAME"
["ragflow"]="RAGFLOW_HOSTNAME"
["waha"]="WAHA_HOSTNAME"
["searxng"]="SEARXNG_HOSTNAME"
["comfyui"]="COMFYUI_HOSTNAME"
["welcome"]="WELCOME_HOSTNAME"
["databasus"]="DATABASUS_HOSTNAME"
["letta"]="LETTA_HOSTNAME"
["lightrag"]="LIGHTRAG_HOSTNAME"
["weaviate"]="WEAVIATE_HOSTNAME"
["qdrant"]="QDRANT_HOSTNAME"
["neo4j"]="NEO4J_HOSTNAME"
["postiz"]="POSTIZ_HOSTNAME"
["libretranslate"]="LT_HOSTNAME"
["paddleocr"]="PADDLEOCR_HOSTNAME"
["docling"]="DOCLING_HOSTNAME"
)
for service in "${!service_map[@]}"; do
local hostname_var="${service_map[$service]}"
local hostname="${!hostname_var:-}"
if [[ -n "$hostname" && "$hostname" != *"yourdomain.com" ]]; then
services+=("$service:$hostname")
fi
done
echo "${services[*]:-}"
}
generate_config() {
local cert_file="$1"
local key_file="$2"
local services=("${@:3}")
cat > "$ADDON_FILE" << 'HEADER'
# Custom TLS Configuration
# Generated by setup_custom_tls.sh
#
# This file overrides default Let's Encrypt certificates with custom ones.
# Regenerate with: make setup-tls
# Reusable TLS snippet
(custom_tls) {
HEADER
echo " tls /etc/caddy/certs/$cert_file /etc/caddy/certs/$key_file" >> "$ADDON_FILE"
echo "}" >> "$ADDON_FILE"
echo "" >> "$ADDON_FILE"
# Service-specific reverse proxy mappings
declare -A proxy_map=(
["n8n"]="n8n:5678"
["flowise"]="flowise:3001"
["webui"]="open-webui:8080"
["grafana"]="grafana:3000"
["prometheus"]="prometheus:9090"
["portainer"]="portainer:9000"
["langfuse"]="langfuse-web:3000"
["supabase"]="kong:8000"
["dify"]="nginx:80"
["nocodb"]="nocodb:8080"
["ragapp"]="ragapp:8000"
["ragflow"]="ragflow:80"
["waha"]="waha:3000"
["searxng"]="searxng:8080"
["comfyui"]="comfyui:8188"
["welcome"]="file_server"
["databasus"]="databasus:4005"
["letta"]="letta:8283"
["lightrag"]="lightrag:9621"
["weaviate"]="weaviate:8080"
["qdrant"]="qdrant:6333"
["neo4j"]="neo4j:7474"
["postiz"]="postiz:5000"
["libretranslate"]="libretranslate:5000"
["paddleocr"]="paddleocr:8080"
["docling"]="docling:5001"
)
# Services that need basic auth (format: USERNAME_VAR:PASSWORD_HASH_VAR)
declare -A auth_services=(
["prometheus"]="PROMETHEUS_USERNAME:PROMETHEUS_PASSWORD_HASH"
["ragapp"]="RAGAPP_USERNAME:RAGAPP_PASSWORD_HASH"
["comfyui"]="COMFYUI_USERNAME:COMFYUI_PASSWORD_HASH"
["welcome"]="WELCOME_USERNAME:WELCOME_PASSWORD_HASH"
["libretranslate"]="LT_USERNAME:LT_PASSWORD_HASH"
["paddleocr"]="PADDLEOCR_USERNAME:PADDLEOCR_PASSWORD_HASH"
["docling"]="DOCLING_USERNAME:DOCLING_PASSWORD_HASH"
)
for service_entry in "${services[@]}"; do
local service="${service_entry%%:*}"
local hostname="${service_entry#*:}"
local proxy="${proxy_map[$service]:-}"
[[ -z "$proxy" ]] && continue
echo "# $service" >> "$ADDON_FILE"
echo "$hostname {" >> "$ADDON_FILE"
echo " import custom_tls" >> "$ADDON_FILE"
# Add basic auth if needed
if [[ -n "${auth_services[$service]:-}" ]]; then
local auth_config="${auth_services[$service]}"
local username_var="${auth_config%%:*}"
local password_hash_var="${auth_config#*:}"
echo " basic_auth {" >> "$ADDON_FILE"
echo " {\$${username_var}} {\$${password_hash_var}}" >> "$ADDON_FILE"
echo " }" >> "$ADDON_FILE"
fi
# Add reverse proxy or file server
if [[ "$proxy" == "file_server" ]]; then
echo " root * /srv/welcome" >> "$ADDON_FILE"
echo " file_server" >> "$ADDON_FILE"
echo " try_files {path} /index.html" >> "$ADDON_FILE"
else
echo " reverse_proxy $proxy" >> "$ADDON_FILE"
fi
echo "}" >> "$ADDON_FILE"
echo "" >> "$ADDON_FILE"
done
log_success "Generated $ADDON_FILE"
}
remove_config() {
if [[ -f "$ADDON_FILE" ]]; then
rm -f "$ADDON_FILE"
log_success "Removed custom TLS configuration"
else
log_info "No custom TLS configuration found"
fi
}
restart_caddy() {
if wt_yesno "Restart Caddy" "Do you want to restart Caddy to apply the new configuration?" "yes"; then
log_info "Restarting Caddy..."
docker compose -p localai restart caddy
log_success "Caddy restarted"
else
log_info "Skipped Caddy restart. Run manually: docker compose -p localai restart caddy"
fi
}
# =============================================================================
# MAIN
# =============================================================================
main() {
# Handle arguments
case "${1:-}" in
-h|--help)
show_help
exit 0
;;
--remove)
remove_config
restart_caddy
exit 0
;;
esac
# Ensure certs directory exists
mkdir -p "$CERTS_DIR"
local cert_file=""
local key_file=""
# Non-interactive mode
if [[ $# -ge 2 ]]; then
cert_file="$1"
key_file="$2"
if [[ ! -f "$CERTS_DIR/$cert_file" ]]; then
log_error "Certificate not found: $CERTS_DIR/$cert_file"
exit 1
fi
if [[ ! -f "$CERTS_DIR/$key_file" ]]; then
log_error "Key not found: $CERTS_DIR/$key_file"
exit 1
fi
else
# Interactive mode
require_whiptail
# Find available certificates
local certs_arr
IFS=' ' read -ra certs_arr <<< "$(find_certificates)"
if [[ ${#certs_arr[@]} -eq 0 ]]; then
wt_msg "No Certificates Found" "No certificate files found in ./certs/\n\nPlease place your certificate (.crt, .pem, .cer) and key (.key) files in the certs/ directory first."
exit 1
fi
# Build menu items for certificates
local cert_items=()
for cert in "${certs_arr[@]}"; do
cert_items+=("$cert" "")
done
cert_file=$(wt_menu "Select Certificate" "Choose your TLS certificate file:" "${cert_items[@]}")
[[ -z "$cert_file" ]] && exit 1
# Find available keys
local keys_arr
IFS=' ' read -ra keys_arr <<< "$(find_keys)"
if [[ ${#keys_arr[@]} -eq 0 ]]; then
wt_msg "No Keys Found" "No key files found in ./certs/\n\nPlease place your private key (.key) file in the certs/ directory."
exit 1
fi
# Build menu items for keys
local key_items=()
for key in "${keys_arr[@]}"; do
key_items+=("$key" "")
done
key_file=$(wt_menu "Select Private Key" "Choose your TLS private key file:" "${key_items[@]}")
[[ -z "$key_file" ]] && exit 1
fi
log_info "Using certificate: $cert_file"
log_info "Using key: $key_file"
# Get active services
local services_arr
IFS=' ' read -ra services_arr <<< "$(get_active_services)"
if [[ ${#services_arr[@]} -eq 0 ]]; then
log_warning "No services with configured hostnames found in .env"
log_info "Make sure to update *_HOSTNAME variables in .env with your domain"
exit 1
fi
log_info "Found ${#services_arr[@]} services with configured hostnames"
# Generate configuration
generate_config "$cert_file" "$key_file" "${services_arr[@]}"
# Show summary
echo ""
log_info "Configuration generated for the following services:"
for service_entry in "${services_arr[@]}"; do
local service="${service_entry%%:*}"
local hostname="${service_entry#*:}"
echo " - $service: $hostname"
done
echo ""
# Restart Caddy
restart_caddy
}
main "$@"