Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ ADMIN_PASSWORD=your_secure_admin_password
USER_EMAIL=user@libredb.org
USER_PASSWORD=your_secure_user_password

# TWO-FACTOR AUTHENTICATION (TOTP) — optional, local provider only
# ============================================
# Base32 secret (RFC 4648: A-Z and 2-7). When set, that account must present a
# 6-digit code from an authenticator app after its password. Opt-in per account:
# set one, both, or neither. Under NEXT_PUBLIC_AUTH_PROVIDER=oidc the login page
# shows no password form, so MFA belongs at the identity provider (docs/OIDC.md)
# — but POST /api/auth/login stays reachable whenever a password is ALSO
# configured, and these secrets guard that route in every mode.
#
# Generate one, then enrol it in your app of choice:
# openssl rand 20 | base32 | tr -d '=' # 160-bit secret, per RFC 4226
# Minimum 26 base32 characters, so the decoded secret carries the 128 bits
# RFC 4226 requires. A value that is not base32, or shorter than that, stops
# login with a clear 503 naming the variable rather than silently disabling or
# silently weakening the second factor. Blank the variable to turn MFA off.
#
# The placeholder below is deliberately not base32, like every other secret in
# this file. A published example secret is the one value that must never be left
# in place: the account would read as protected everywhere while anyone could
# compute its codes. Uncommented as-is, this earns the 503 instead.
# ADMIN_TOTP_SECRET=your_base32_secret_from_the_command_above
# USER_TOTP_SECRET=

# JWT Secret for session management (min 32 characters)
# Generate with: openssl rand -base64 32
# A shorter value stops the server at startup (exit code 1) instead of booting
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ extraEnvFrom:
| [Local models](docs/llms/README.md) | Which local model can actually drive an agent run, measured across three workflows, one page per model |
| [Agent Runtime](docs/AGENT.md) | Agent behaviour, bounds, deployment and known limitations |
| [OIDC SSO](docs/OIDC.md) | SSO setup (Auth0, Keycloak, Okta, Azure AD, Zitadel, Google) + subsystem internals & security model |
| [Two-Factor Auth](docs/MFA.md) | TOTP on the local provider — generating a secret, enrolling an app, Docker/Helm wiring, and what it does not cover |
| [Theming Guide](docs/ui/theming.md) | CSS theming, dark mode, and styling customization |
| [Login Page](docs/ui/login-page.md) | Login page layout, OIDC/local modes, and design system |
| [Editor Docs](docs/editor/) | SQL editor internals — completion, performance, query optimization |
Expand Down
2 changes: 1 addition & 1 deletion charts/libredb-studio/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: libredb-studio
description: Web-based SQL IDE for cloud-native teams supporting sixteen engines - PostgreSQL, MySQL, SQLite, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Apache Trino, Apache Cassandra and libSQL
type: application
version: 0.1.62
version: 0.1.63
appVersion: "0.15.0"
kubeVersion: ">=1.26.0-0"
home: https://github.com/libredb/libredb-studio
Expand Down
36 changes: 34 additions & 2 deletions charts/libredb-studio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ helm install libredb libredb/libredb-studio \

```bash
helm install libredb oci://ghcr.io/libredb/charts/libredb-studio \
--version 0.1.62 \
--version 0.1.63 \
--set secrets.jwtSecret=$(openssl rand -base64 32) \
--set secrets.adminPassword=MyAdmin123
```
Expand Down Expand Up @@ -147,6 +147,36 @@ browser still speaks `https`, so it accepts the cookie. `false` is only for the
the browser's own connection is cleartext - and it means session cookies travel in
cleartext, so keep it to a trusted network. `true` forces the flag on.

## Two-Factor Authentication (TOTP)

Optional, local-provider only, and opt-in per account. Set a base32 secret and that account must
present a 6-digit authenticator code after its password:

```bash
# Generate it, enrol the printed value in your authenticator app, then install.
ADMIN_TOTP_SECRET="$(openssl rand 20 | base32 | tr -d '=')"
echo "$ADMIN_TOTP_SECRET"

helm upgrade --install libredb libredb/libredb-studio \
--set secrets.adminPassword=MyAdmin123 \
--set secrets.adminTotpSecret="$ADMIN_TOTP_SECRET"
```

No example secret is printed here on purpose. One that looks real invites being copied and left in
place, and a second factor whose secret is published is worse than none.

The value travels in the chart's Secret and is referenced from the pod, so it never appears in the
Deployment spec - which is why `extraEnv` is the wrong tool for it. Both `ADMIN_TOTP_SECRET` and
`USER_TOTP_SECRET` refs are always optional, including in strict mode, so a second factor nobody
asked for can never keep the pod from starting. `values.schema.json` applies the same test the app
does, base32 and at least the 128 bits RFC 4226 requires, so a bad secret fails at install time
rather than at the login screen.

Under `authProvider=oidc` the login page shows no password form and MFA belongs to the identity
provider - but `POST /api/auth/login` stays reachable whenever `secrets.adminPassword` is also set,
and this guards that route in every mode. Full setup, enrolment and recovery:
[`docs/MFA.md`](../../docs/MFA.md).

## OIDC SSO

```bash
Expand Down Expand Up @@ -542,7 +572,7 @@ helm install libredb libredb/libredb-studio \

Your external secret is referenced with these keys (customizable via `secrets.existingSecretKeys`):
- `jwt-secret`, `admin-password` — required in strict mode (the pod waits for them); in zero-config mode missing ones are generated at first start
- Optional: `admin-email`, `user-email`, `user-password` (the non-admin account exists only when `user-password` is set), `llm-api-key`, `oidc-client-id`, `oidc-client-secret`, `storage-postgres-url`
- Optional: `admin-email`, `user-email`, `user-password` (the non-admin account exists only when `user-password` is set), `admin-totp-secret`, `user-totp-secret`, `llm-api-key`, `oidc-client-id`, `oidc-client-secret`, `storage-postgres-url`

## Upgrading

Expand Down Expand Up @@ -586,6 +616,8 @@ helm uninstall libredb
| `secrets.adminPassword` | Admin password | `""` |
| `secrets.userEmail` | User email | `user@libredb.org` |
| `secrets.userPassword` | User password (optional; enables the non-admin account) | `""` |
| `secrets.adminTotpSecret` | Base32 TOTP secret for the admin account (optional second factor) | `""` |
| `secrets.userTotpSecret` | Base32 TOTP secret for the user account (optional second factor) | `""` |
| `secrets.existingSecret` | Use existing Secret | `""` |
| `config.bindAddress` | Container bind address (`HOSTNAME`): empty lets the image resolve one, preferring a verified dual-stack `::`; `::` forces it; `0.0.0.0` pins IPv4 | `""` |
| `config.storageProvider` | Storage: local, sqlite, postgres | `local` |
Expand Down
20 changes: 20 additions & 0 deletions charts/libredb-studio/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,26 @@ spec:
key: {{ .Values.secrets.existingSecretKeys.userPassword }}
optional: true
{{- end }}
{{- /* TOTP second factor. Always optional: MFA is opt-in per account, so a missing
key must never keep the pod from starting, and the app treats an absent value
as "no second factor". Keyed separately from the passwords rather than nested
under them so an operator can rotate a secret without touching a credential. */}}
{{- if or .Values.secrets.adminTotpSecret .Values.secrets.existingSecret }}
- name: ADMIN_TOTP_SECRET
valueFrom:
secretKeyRef:
name: {{ include "libredb-studio.secretName" . }}
key: {{ .Values.secrets.existingSecretKeys.adminTotpSecret }}
optional: true
{{- end }}
{{- if or .Values.secrets.userTotpSecret .Values.secrets.existingSecret }}
- name: USER_TOTP_SECRET
valueFrom:
secretKeyRef:
name: {{ include "libredb-studio.secretName" . }}
key: {{ .Values.secrets.existingSecretKeys.userTotpSecret }}
optional: true
{{- end }}
{{- if or .Values.secrets.llmApiKey .Values.secrets.existingSecret }}
- name: LLM_API_KEY
valueFrom:
Expand Down
6 changes: 6 additions & 0 deletions charts/libredb-studio/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ data:
{{ .Values.secrets.existingSecretKeys.userEmail }}: {{ .Values.secrets.userEmail | b64enc | quote }}
{{ .Values.secrets.existingSecretKeys.userPassword }}: {{ .Values.secrets.userPassword | b64enc | quote }}
{{- end }}
{{- if .Values.secrets.adminTotpSecret }}
{{ .Values.secrets.existingSecretKeys.adminTotpSecret }}: {{ .Values.secrets.adminTotpSecret | b64enc | quote }}
{{- end }}
{{- if .Values.secrets.userTotpSecret }}
{{ .Values.secrets.existingSecretKeys.userTotpSecret }}: {{ .Values.secrets.userTotpSecret | b64enc | quote }}
{{- end }}
{{- if .Values.secrets.llmApiKey }}
{{ .Values.secrets.existingSecretKeys.llmApiKey }}: {{ .Values.secrets.llmApiKey | b64enc | quote }}
{{- end }}
Expand Down
10 changes: 10 additions & 0 deletions charts/libredb-studio/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@
"type": "string",
"description": "User account password"
},
"adminTotpSecret": {
"type": "string",
"pattern": "^$|^[-\\s]*(?:[A-Za-z2-7][-\\s]*){26,}=*$",
"description": "Admin TOTP secret: empty (no second factor) or base32 (RFC 4648: A-Z and 2-7; spaces, hyphens and trailing = are stripped), at least 26 characters so the decoded secret carries the 128 bits RFC 4226 requires"
},
"userTotpSecret": {
"type": "string",
"pattern": "^$|^[-\\s]*(?:[A-Za-z2-7][-\\s]*){26,}=*$",
"description": "User TOTP secret: empty (no second factor) or base32 (RFC 4648: A-Z and 2-7; spaces, hyphens and trailing = are stripped), at least 26 characters so the decoded secret carries the 128 bits RFC 4226 requires"
},
"llmApiKey": {
"type": "string",
"description": "LLM API key"
Expand Down
13 changes: 13 additions & 0 deletions charts/libredb-studio/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ secrets:
adminPassword: admin-password
userEmail: user-email
userPassword: user-password
adminTotpSecret: admin-totp-secret
userTotpSecret: user-totp-secret
llmApiKey: llm-api-key
oidcClientId: oidc-client-id
oidcClientSecret: oidc-client-secret
Expand All @@ -75,6 +77,17 @@ secrets:
# -- Regular user account password. Optional: the non-admin account exists only when set;
# it is never generated.
userPassword: ""
# -- Base32 TOTP secret for the admin account (RFC 4648: A-Z and 2-7), at least 26 characters
# so the decoded secret carries the 128 bits RFC 4226 requires. Optional and
# local-provider only: when set, admin login requires a 6-digit authenticator code after the
# password. A value that is not base32 stops login with a clear 503 rather than silently
# dropping the second factor. Under config.authProvider=oidc the login page shows no password
# form and MFA belongs to the identity provider, but this still guards POST /api/auth/login
# whenever a password is also set. Generate one with: openssl rand 20 | base32 | tr -d '='
adminTotpSecret: ""
# -- Base32 TOTP secret for the regular user account. Same rules as adminTotpSecret; inert
# unless userPassword is also set, since without it there is no user account to protect.
userTotpSecret: ""
# -- LLM API key (optional, for AI features)
llmApiKey: ""
# -- OIDC client ID (required when authProvider=oidc)
Expand Down
Loading
Loading