Compare commits

..

4 Commits

Author SHA1 Message Date
hsiegeln
0125694572 chore: ignore local install dir created by installers
The install scripts default to ./cameleer/ for local install state.
Ignoring it prevents accidental commits of generated .env files,
TLS certs, and bootstrap data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:43:26 +02:00
hsiegeln
f7a57edacc docs: update default registry to registry.cameleer.io and drop JWT row
Updates the documented default registry from gitea.siegeln.net/cameleer
to registry.cameleer.io/cameleer in both CLAUDE.md and README.md, and
removes the now-obsolete CAMELEER_SERVER_SECURITY_JWTSECRET row from
the auto-generated secrets table to match the prior installer change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:43:26 +02:00
hsiegeln
7bb9ef3283 refactor: drop JWT secret env vars from installers and compose
Removes CAMELEER_SERVER_SECURITY_JWTSECRET and
CAMELEER_SAAS_PROVISIONING_JWTSECRET — a better solution has been
adopted upstream, so the installers no longer need to generate or
forward a shared JWT secret.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:40:35 +02:00
hsiegeln
531a17397b fix: validate admin email format in SaaS mode
Require user@domain.tld format (must contain @ and dot in domain).
Interactive mode loops until valid; silent mode exits with error.
Default changed from 'admin' to 'admin@<PUBLIC_HOST>' in SaaS mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 21:03:45 +02:00
7 changed files with 39 additions and 33 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Local install state created by install.sh / install.ps1 (default --install-dir)
/cameleer/

View File

@@ -31,7 +31,7 @@ Previously, SMTP env vars (`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `
## Registry configuration ## Registry configuration
The installer supports pulling images from a custom Docker registry via `--registry`. Default: `gitea.siegeln.net/cameleer`. The installer supports pulling images from a custom Docker registry via `--registry`. Default: `registry.cameleer.io/cameleer`.
When a registry is configured, the installer writes `*_IMAGE` env vars to `.env` (e.g. `TRAEFIK_IMAGE`, `POSTGRES_IMAGE`, `CAMELEER_IMAGE`) which override the defaults baked into the compose templates. In SaaS mode, provisioning image refs (`CAMELEER_SAAS_PROVISIONING_*IMAGE`) are also set from the registry. When a registry is configured, the installer writes `*_IMAGE` env vars to `.env` (e.g. `TRAEFIK_IMAGE`, `POSTGRES_IMAGE`, `CAMELEER_IMAGE`) which override the defaults baked into the compose templates. In SaaS mode, provisioning image refs (`CAMELEER_SAAS_PROVISIONING_*IMAGE`) are also set from the registry.

View File

@@ -135,7 +135,7 @@ The Docker socket is required for tenant provisioning (SaaS mode) — the platfo
| Setting | CLI Flag | Env Var | Config Key | Default | | Setting | CLI Flag | Env Var | Config Key | Default |
|---------|----------|---------|------------|---------| |---------|----------|---------|------------|---------|
| Registry | `--registry` | `REGISTRY` | `registry` | `gitea.siegeln.net/cameleer` | | Registry | `--registry` | `REGISTRY` | `registry` | `registry.cameleer.io/cameleer` |
| Registry username | `--registry-user` | `REGISTRY_USER` | `registry_user` | — | | Registry username | `--registry-user` | `REGISTRY_USER` | `registry_user` | — |
| Registry token | `--registry-token` | `REGISTRY_TOKEN` | `registry_token` | — | | Registry token | `--registry-token` | `REGISTRY_TOKEN` | `registry_token` | — |
| Image version | `--version` | `VERSION` | `version` | `latest` | | Image version | `--version` | `VERSION` | `version` | `latest` |
@@ -172,7 +172,6 @@ These are generated automatically and never need to be set manually:
| Secret | Env Var | Description | | Secret | Env Var | Description |
|--------|---------|-------------| |--------|---------|-------------|
| JWT signing secret | `CAMELEER_SERVER_SECURITY_JWTSECRET` | Shared secret for JWT token signing across provisioned tenant servers |
| Bootstrap token | `BOOTSTRAP_TOKEN` | Server initialization token (standalone mode only) | | Bootstrap token | `BOOTSTRAP_TOKEN` | Server initialization token (standalone mode only) |
--- ---

View File

@@ -465,15 +465,15 @@ function Run-SimplePrompts {
Write-Host '' Write-Host ''
if ($c.DeploymentMode -eq 'saas') { if ($c.DeploymentMode -eq 'saas') {
$defaultEmail = Coalesce $c.AdminUser "admin@$($c.PublicHost)" $defaultEmail = Coalesce $c.AdminUser "admin@$($c.PublicHost)"
if ($defaultEmail -and -not $defaultEmail.Contains('@')) { if ($defaultEmail -and $defaultEmail -notmatch '@.+\..+') {
$defaultEmail = "admin@$($c.PublicHost)" $defaultEmail = "admin@$($c.PublicHost)"
} }
$c.AdminUser = Prompt-Value 'Admin email' $defaultEmail while ($true) {
# Validate email: must contain @ $c.AdminUser = Prompt-Value 'Admin email' $defaultEmail
if (-not $c.AdminUser.Contains('@')) { if ($c.AdminUser -match '^[^@]+@[^@]+\.[^@]+$') { break }
$original = $c.AdminUser Write-Host ' Invalid email address. Must be a valid email (e.g. admin@company.com).' -ForegroundColor Red
$c.AdminUser = "$($c.AdminUser)@$($c.PublicHost)" $c.AdminUser = $null
Log-Info "Appended domain: '$original' -> '$($c.AdminUser)'" $defaultEmail = $null
} }
} else { } else {
$c.AdminUser = Prompt-Value 'Admin username' (Coalesce $c.AdminUser $DEFAULT_ADMIN_USER) $c.AdminUser = Prompt-Value 'Admin username' (Coalesce $c.AdminUser $DEFAULT_ADMIN_USER)
@@ -557,7 +557,15 @@ function Merge-Config {
if (-not $c.InstallDir) { $c.InstallDir = $DEFAULT_INSTALL_DIR } if (-not $c.InstallDir) { $c.InstallDir = $DEFAULT_INSTALL_DIR }
if (-not $c.PublicHost) { $c.PublicHost = 'localhost' } if (-not $c.PublicHost) { $c.PublicHost = 'localhost' }
if (-not $c.PublicProtocol) { $c.PublicProtocol = $DEFAULT_PUBLIC_PROTOCOL } if (-not $c.PublicProtocol) { $c.PublicProtocol = $DEFAULT_PUBLIC_PROTOCOL }
if (-not $c.AdminUser) { $c.AdminUser = $DEFAULT_ADMIN_USER } if ($c.DeploymentMode -eq 'saas') {
if (-not $c.AdminUser) { $c.AdminUser = "admin@$($c.PublicHost)" }
if ($c.AdminUser -notmatch '^[^@]+@[^@]+\.[^@]+$') {
Write-Host "ERROR: SAAS_ADMIN_USER must be a valid email address (e.g. admin@company.com), got: '$($c.AdminUser)'" -ForegroundColor Red
exit 1
}
} else {
if (-not $c.AdminUser) { $c.AdminUser = $DEFAULT_ADMIN_USER }
}
if (-not $c.TlsMode) { $c.TlsMode = $DEFAULT_TLS_MODE } if (-not $c.TlsMode) { $c.TlsMode = $DEFAULT_TLS_MODE }
if (-not $c.HttpPort) { $c.HttpPort = $DEFAULT_HTTP_PORT } if (-not $c.HttpPort) { $c.HttpPort = $DEFAULT_HTTP_PORT }
if (-not $c.HttpsPort) { $c.HttpsPort = $DEFAULT_HTTPS_PORT } if (-not $c.HttpsPort) { $c.HttpsPort = $DEFAULT_HTTPS_PORT }
@@ -650,7 +658,6 @@ function Generate-EnvFile {
$ts = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' UTC' $ts = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' UTC'
$bt = Generate-Password $bt = Generate-Password
$jwtSecret = Generate-Password
if ($c.DeploymentMode -eq 'standalone') { if ($c.DeploymentMode -eq 'standalone') {
$content = @" $content = @"
@@ -673,9 +680,6 @@ SERVER_ADMIN_USER=$($c.AdminUser)
# Bootstrap token # Bootstrap token
BOOTSTRAP_TOKEN=$bt BOOTSTRAP_TOKEN=$bt
# JWT signing secret (required by server, must be non-empty)
CAMELEER_SERVER_SECURITY_JWTSECRET=$jwtSecret
# Docker # Docker
DOCKER_SOCKET=$($c.DockerSocket) DOCKER_SOCKET=$($c.DockerSocket)
DOCKER_GID=$gid DOCKER_GID=$gid
@@ -755,8 +759,6 @@ CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=$reg/cameleer-server:$($c.Version)
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=$reg/cameleer-server-ui:$($c.Version) CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=$reg/cameleer-server-ui:$($c.Version)
CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE=$reg/cameleer-runtime-base:$($c.Version) CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE=$reg/cameleer-runtime-base:$($c.Version)
# JWT signing secret (forwarded to provisioned tenant servers, must be non-empty)
CAMELEER_SERVER_SECURITY_JWTSECRET=$jwtSecret
"@ "@
$content += $provisioningBlock $content += $provisioningBlock
# Passwords appended with single-quoting for special character safety # Passwords appended with single-quoting for special character safety

View File

@@ -448,13 +448,15 @@ run_simple_prompts() {
echo "" echo ""
if [ "$DEPLOYMENT_MODE" = "saas" ]; then if [ "$DEPLOYMENT_MODE" = "saas" ]; then
prompt ADMIN_USER "Admin email" "${ADMIN_USER:-admin@${PUBLIC_HOST:-localhost}}" while true; do
# Validate email: must contain @ prompt ADMIN_USER "Admin email" "${ADMIN_USER:-admin@${PUBLIC_HOST:-localhost}}"
if ! echo "$ADMIN_USER" | grep -q '@'; then # Validate email: must be user@domain.tld format
local original="$ADMIN_USER" if echo "$ADMIN_USER" | grep -qE '^[^@]+@[^@]+\.[^@]+$'; then
ADMIN_USER="${ADMIN_USER}@${PUBLIC_HOST:-localhost}" break
log_info "Appended domain: '${original}' -> '${ADMIN_USER}'" fi
fi echo -e " ${RED}Invalid email address.${NC} Must be a valid email (e.g. admin@company.com)."
ADMIN_USER=""
done
else else
prompt ADMIN_USER "Admin username" "${ADMIN_USER:-$DEFAULT_ADMIN_USER}" prompt ADMIN_USER "Admin username" "${ADMIN_USER:-$DEFAULT_ADMIN_USER}"
fi fi
@@ -538,7 +540,16 @@ merge_config() {
: "${INSTALL_DIR:=$DEFAULT_INSTALL_DIR}" : "${INSTALL_DIR:=$DEFAULT_INSTALL_DIR}"
: "${PUBLIC_HOST:=localhost}" : "${PUBLIC_HOST:=localhost}"
: "${PUBLIC_PROTOCOL:=$DEFAULT_PUBLIC_PROTOCOL}" : "${PUBLIC_PROTOCOL:=$DEFAULT_PUBLIC_PROTOCOL}"
: "${ADMIN_USER:=$DEFAULT_ADMIN_USER}" if [ "$DEPLOYMENT_MODE" = "saas" ]; then
: "${ADMIN_USER:=admin@${PUBLIC_HOST}}"
# Validate email format in SaaS mode
if ! echo "$ADMIN_USER" | grep -qE '^[^@]+@[^@]+\.[^@]+$'; then
echo -e "${RED}ERROR:${NC} SAAS_ADMIN_USER must be a valid email address (e.g. admin@company.com), got: '$ADMIN_USER'" >&2
exit 1
fi
else
: "${ADMIN_USER:=$DEFAULT_ADMIN_USER}"
fi
: "${TLS_MODE:=$DEFAULT_TLS_MODE}" : "${TLS_MODE:=$DEFAULT_TLS_MODE}"
: "${HTTP_PORT:=$DEFAULT_HTTP_PORT}" : "${HTTP_PORT:=$DEFAULT_HTTP_PORT}"
: "${HTTPS_PORT:=$DEFAULT_HTTPS_PORT}" : "${HTTPS_PORT:=$DEFAULT_HTTPS_PORT}"
@@ -661,9 +672,6 @@ SERVER_ADMIN_USER=${ADMIN_USER}
# Bootstrap token (required by server, not used externally in standalone mode) # Bootstrap token (required by server, not used externally in standalone mode)
BOOTSTRAP_TOKEN=$(generate_password) BOOTSTRAP_TOKEN=$(generate_password)
# JWT signing secret (required by server, must be non-empty)
CAMELEER_SERVER_SECURITY_JWTSECRET=$(generate_password)
# Docker # Docker
DOCKER_SOCKET=${DOCKER_SOCKET} DOCKER_SOCKET=${DOCKER_SOCKET}
DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0") DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0")
@@ -755,9 +763,6 @@ CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=${REGISTRY}/cameleer-server:${VERSION}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer-server-ui:${VERSION} CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer-server-ui:${VERSION}
CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE=${REGISTRY}/cameleer-runtime-base:${VERSION} CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE=${REGISTRY}/cameleer-runtime-base:${VERSION}
# JWT signing secret (forwarded to provisioned tenant servers, must be non-empty)
CAMELEER_SERVER_SECURITY_JWTSECRET=$(generate_password)
# Compose file assembly # Compose file assembly
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml") COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml")
EOF EOF

View File

@@ -85,7 +85,6 @@ services:
CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer} CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer}
CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD} CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD}
CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD} CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD}
CAMELEER_SERVER_SECURITY_JWTSECRET: ${CAMELEER_SERVER_SECURITY_JWTSECRET:?CAMELEER_SERVER_SECURITY_JWTSECRET must be set in .env}
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-registry.cameleer.io/cameleer/cameleer-server:latest} CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-registry.cameleer.io/cameleer/cameleer-server:latest}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-registry.cameleer.io/cameleer/cameleer-server-ui:latest} CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-registry.cameleer.io/cameleer/cameleer-server-ui:latest}
CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE: ${CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE:-registry.cameleer.io/cameleer/cameleer-runtime-base:latest} CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE: ${CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE:-registry.cameleer.io/cameleer/cameleer-runtime-base:latest}

View File

@@ -29,7 +29,6 @@ services:
CAMELEER_SERVER_CLICKHOUSE_USERNAME: default CAMELEER_SERVER_CLICKHOUSE_USERNAME: default
CAMELEER_SERVER_CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD} CAMELEER_SERVER_CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN: ${BOOTSTRAP_TOKEN:?BOOTSTRAP_TOKEN must be set in .env} CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN: ${BOOTSTRAP_TOKEN:?BOOTSTRAP_TOKEN must be set in .env}
CAMELEER_SERVER_SECURITY_JWTSECRET: ${CAMELEER_SERVER_SECURITY_JWTSECRET:?CAMELEER_SERVER_SECURITY_JWTSECRET must be set in .env}
CAMELEER_SERVER_SECURITY_UIUSER: ${SERVER_ADMIN_USER:-admin} CAMELEER_SERVER_SECURITY_UIUSER: ${SERVER_ADMIN_USER:-admin}
CAMELEER_SERVER_SECURITY_UIPASSWORD: ${SERVER_ADMIN_PASS:?SERVER_ADMIN_PASS must be set in .env} CAMELEER_SERVER_SECURITY_UIPASSWORD: ${SERVER_ADMIN_PASS:?SERVER_ADMIN_PASS must be set in .env}
CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost} CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}