mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 16:47:32 +00:00
docs(self-hosting): add login to lb example (#9496)
# Which Problems Are Solved We have no docs for self-hosting the login using the standard login as a standalone docker container. # How the Problems Are Solved A common self-hosting case is to publish the login at the same domain as Zitadel behind a reverse proxy. That's why we extend the load balancing example. We refocus the example from *making TLS work* to *running multiple services behind the proxy and connect them using an internal network and DNS*. I decided this together with @fforootd. For authenticating with the login application, we have to set up a service user and give it the role IAM_LOGIN_CLIENT. We do so in the use-new-login "job" container as `zitadel setup` only supports Zitadel users with the role IAM_ADMIN AFAIR. The login application relies on a healthy Zitadel API on startup, which is why we fix the containers readiness reports. # Additional Changes - We deploy the init and setup jobs independently, because this better reflects our production recommendatinons. It gives more control over the upgrade process. - We use the ExternalDomain *127.0.0.1.sslip.io* instead of *my.domain*, because this doesn't require changing the local DNS resolution by changing */etc/hosts* for local tests. # Testing The commands in the preview docs use to the configuration files on main. This is fine when the PR is merged but not for testing the PR. Replace the used links to make them point to the PRs changed files. Instead of the commands in the preview docs, use these: ```bash # Download the docker compose example configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml # Download the Traefik example configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml # Download and adjust the example configuration file containing standard configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml # Download and adjust the example configuration file containing secret configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-secrets.yaml # Download and adjust the example configuration file containing database initialization configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml # A single ZITADEL instance always needs the same 32 bytes long masterkey # Generate one to a file if you haven't done so already and pass it as environment variable LC_ALL=C tr -dc '[:graph:]' </dev/urandom | head -c 32 > ./zitadel-masterkey export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)" # Run the database and application containers docker compose up --detach --wait ``` # Additional Context - Closes https://github.com/zitadel/DevOps/issues/111 - Depends on https://github.com/zitadel/typescript/pull/412 - Contributes to road map item https://github.com/zitadel/zitadel/issues/9481
This commit is contained in:
1
docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore
vendored
Normal file
1
docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env-file
|
@@ -1,48 +1,157 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
traefik:
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=root
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
networks:
|
networks:
|
||||||
- 'zitadel'
|
- 'storage'
|
||||||
image: "traefik:latest"
|
healthcheck:
|
||||||
ports:
|
test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
|
||||||
- "80:80"
|
interval: 10s
|
||||||
- "443:443"
|
timeout: 60s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
volumes:
|
volumes:
|
||||||
- "./example-traefik.yaml:/etc/traefik/traefik.yaml"
|
- 'data:/var/lib/postgresql/data:rw'
|
||||||
|
|
||||||
zitadel:
|
zitadel-init:
|
||||||
restart: 'always'
|
restart: 'no'
|
||||||
networks:
|
networks:
|
||||||
- 'zitadel'
|
- 'storage'
|
||||||
image: 'ghcr.io/zitadel/zitadel:latest'
|
image: 'ghcr.io/zitadel/zitadel:latest'
|
||||||
command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode external'
|
command: 'init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml'
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: 'service_healthy'
|
condition: 'service_healthy'
|
||||||
volumes:
|
volumes:
|
||||||
- './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
|
- './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
|
||||||
- './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
|
- './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
|
||||||
|
|
||||||
|
zitadel-setup:
|
||||||
|
restart: 'no'
|
||||||
|
networks:
|
||||||
|
- 'storage'
|
||||||
|
# We use the debug image so we have the environment to
|
||||||
|
# - create the .env file for the login to authenticate at Zitadel
|
||||||
|
# - set the correct permissions for the .env-file folder
|
||||||
|
image: 'ghcr.io/zitadel/zitadel:latest-debug'
|
||||||
|
user: root
|
||||||
|
entrypoint: '/bin/sh'
|
||||||
|
command:
|
||||||
|
- -c
|
||||||
|
- >
|
||||||
|
/app/zitadel setup
|
||||||
|
--config /example-zitadel-config.yaml
|
||||||
|
--config /example-zitadel-secrets.yaml
|
||||||
|
--steps /example-zitadel-init-steps.yaml
|
||||||
|
--masterkey ${ZITADEL_MASTERKEY} &&
|
||||||
|
mv /pat /.env-file/pat || exit 0 &&
|
||||||
|
echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env &&
|
||||||
|
chown -R 1001:${GID} /.env-file &&
|
||||||
|
chmod -R 770 /.env-file
|
||||||
|
environment:
|
||||||
|
- GID
|
||||||
|
depends_on:
|
||||||
|
zitadel-init:
|
||||||
|
condition: 'service_completed_successfully'
|
||||||
|
restart: false
|
||||||
|
volumes:
|
||||||
|
- './.env-file:/.env-file:rw'
|
||||||
|
- './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
|
||||||
|
- './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
|
||||||
- './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro'
|
- './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro'
|
||||||
|
|
||||||
db:
|
zitadel:
|
||||||
image: postgres:17-alpine
|
restart: 'unless-stopped'
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=root
|
|
||||||
- POSTGRES_PASSWORD=postgres
|
|
||||||
networks:
|
networks:
|
||||||
- 'zitadel'
|
- 'backend'
|
||||||
|
- 'storage'
|
||||||
|
image: 'ghcr.io/zitadel/zitadel:latest'
|
||||||
|
command: >
|
||||||
|
start --config /example-zitadel-config.yaml
|
||||||
|
--config /example-zitadel-secrets.yaml
|
||||||
|
--masterkey ${ZITADEL_MASTERKEY}
|
||||||
|
depends_on:
|
||||||
|
zitadel-setup:
|
||||||
|
condition: 'service_completed_successfully'
|
||||||
|
restart: true
|
||||||
|
volumes:
|
||||||
|
- './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
|
||||||
|
- './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
|
test: [
|
||||||
|
"CMD", "/app/zitadel", "ready",
|
||||||
|
"--config", "/example-zitadel-config.yaml",
|
||||||
|
"--config", "/example-zitadel-secrets.yaml"
|
||||||
|
]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 60s
|
timeout: 60s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
|
# The use-new-login service configures Zitadel to use the new login v2 for all applications.
|
||||||
|
# It also gives the setupped machine user the necessary IAM_LOGIN_CLIENT role.
|
||||||
|
use-new-login:
|
||||||
|
restart: 'on-failure'
|
||||||
|
user: "1001"
|
||||||
|
networks:
|
||||||
|
- 'backend'
|
||||||
|
image: 'badouralix/curl-jq:alpine'
|
||||||
|
entrypoint: '/bin/sh'
|
||||||
|
command:
|
||||||
|
- -c
|
||||||
|
- >
|
||||||
|
curl -X PUT -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/v2/features/instance -d '{"loginV2": {"required": true}}' &&
|
||||||
|
LOGIN_USER=$(curl --fail-with-body -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/auth/v1/users/me | jq -r '.user.id') &&
|
||||||
|
curl -X PUT -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/admin/v1/members/$${LOGIN_USER} -d '{"roles": ["IAM_OWNER", "IAM_LOGIN_CLIENT"]}'
|
||||||
volumes:
|
volumes:
|
||||||
- 'data:/var/lib/postgresql/data:rw'
|
- './.env-file:/.env-file:ro'
|
||||||
|
depends_on:
|
||||||
|
zitadel:
|
||||||
|
condition: 'service_healthy'
|
||||||
|
restart: false
|
||||||
|
|
||||||
|
login:
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
networks:
|
||||||
|
- 'backend'
|
||||||
|
image: 'ghcr.io/zitadel/login:main'
|
||||||
|
environment:
|
||||||
|
- ZITADEL_API_URL=http://zitadel:8080
|
||||||
|
- CUSTOM_REQUEST_HEADERS=Host:127.0.0.1.sslip.io
|
||||||
|
- NEXT_PUBLIC_BASE_PATH="/ui/v2/login"
|
||||||
|
user: "${UID:-1000}"
|
||||||
|
volumes:
|
||||||
|
- './.env-file:/.env-file:ro'
|
||||||
|
depends_on:
|
||||||
|
zitadel:
|
||||||
|
condition: 'service_healthy'
|
||||||
|
restart: false
|
||||||
|
|
||||||
|
traefik:
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
networks:
|
||||||
|
- 'backend'
|
||||||
|
image: "traefik:latest"
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- "./example-traefik.yaml:/etc/traefik/traefik.yaml"
|
||||||
|
depends_on:
|
||||||
|
zitadel:
|
||||||
|
condition: 'service_healthy'
|
||||||
|
login:
|
||||||
|
condition: 'service_started'
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
zitadel:
|
storage:
|
||||||
|
backend:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
|
@@ -4,66 +4,37 @@ log:
|
|||||||
accessLog: {}
|
accessLog: {}
|
||||||
|
|
||||||
entrypoints:
|
entrypoints:
|
||||||
web:
|
|
||||||
address: ":80"
|
|
||||||
|
|
||||||
websecure:
|
websecure:
|
||||||
address: ":443"
|
address: ":443"
|
||||||
|
|
||||||
tls:
|
|
||||||
stores:
|
|
||||||
default:
|
|
||||||
# generates self-signed certificates
|
|
||||||
defaultCertificate:
|
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
file:
|
file:
|
||||||
filename: /etc/traefik/traefik.yaml
|
filename: /etc/traefik/traefik.yaml
|
||||||
|
|
||||||
http:
|
http:
|
||||||
middlewares:
|
|
||||||
zitadel:
|
|
||||||
headers:
|
|
||||||
isDevelopment: false
|
|
||||||
allowedHosts:
|
|
||||||
- 'my.domain'
|
|
||||||
customRequestHeaders:
|
|
||||||
authority: 'my.domain'
|
|
||||||
redirect-to-https:
|
|
||||||
redirectScheme:
|
|
||||||
scheme: https
|
|
||||||
port: 443
|
|
||||||
permanent: true
|
|
||||||
|
|
||||||
routers:
|
routers:
|
||||||
# Redirect HTTP to HTTPS
|
login:
|
||||||
router0:
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- web
|
- websecure
|
||||||
middlewares:
|
service: login
|
||||||
- redirect-to-https
|
rule: 'Host(`127.0.0.1.sslip.io`) && PathPrefix(`/ui/v2/login`)'
|
||||||
rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)'
|
tls: {}
|
||||||
service: zitadel
|
zitadel:
|
||||||
# The actual ZITADEL router
|
|
||||||
router1:
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
service: zitadel
|
service: zitadel
|
||||||
middlewares:
|
rule: 'Host(`127.0.0.1.sslip.io`) && !PathPrefix(`/ui/v2/login`)'
|
||||||
- zitadel
|
tls: {}
|
||||||
rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)'
|
|
||||||
tls:
|
|
||||||
domains:
|
|
||||||
- main: "my.domain"
|
|
||||||
sans:
|
|
||||||
- "*.my.domain"
|
|
||||||
- "my.domain"
|
|
||||||
|
|
||||||
# Add the service
|
|
||||||
services:
|
services:
|
||||||
|
login:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://login:3000
|
||||||
|
passHostHeader: true
|
||||||
zitadel:
|
zitadel:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
# h2c is the scheme for unencrypted HTTP/2
|
|
||||||
- url: h2c://zitadel:8080
|
- url: h2c://zitadel:8080
|
||||||
passHostHeader: true
|
passHostHeader: true
|
||||||
|
|
||||||
|
@@ -1,26 +1,29 @@
|
|||||||
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
|
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
|
||||||
Log:
|
|
||||||
Level: 'info'
|
|
||||||
|
|
||||||
# Make ZITADEL accessible over HTTPs, not HTTP
|
|
||||||
ExternalSecure: true
|
ExternalSecure: true
|
||||||
ExternalDomain: my.domain
|
ExternalDomain: 127.0.0.1.sslip.io
|
||||||
ExternalPort: 443
|
ExternalPort: 443
|
||||||
|
|
||||||
|
# Traefik terminates TLS. Inside the Docker network, we use plain text.
|
||||||
|
TLS.Enabled: false
|
||||||
|
|
||||||
# If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL
|
# If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL
|
||||||
Database:
|
Database:
|
||||||
postgres:
|
postgres:
|
||||||
Host: 'db'
|
Host: 'db'
|
||||||
Port: 5432
|
Port: 5432
|
||||||
Database: zitadel
|
Database: zitadel
|
||||||
User:
|
User.SSL.Mode: 'disable'
|
||||||
SSL:
|
Admin.SSL.Mode: 'disable'
|
||||||
Mode: 'disable'
|
|
||||||
Admin:
|
|
||||||
SSL:
|
|
||||||
Mode: 'disable'
|
|
||||||
|
|
||||||
LogStore:
|
# By default, ZITADEL should redirect to /ui/v2/login
|
||||||
Access:
|
OIDC:
|
||||||
Stdout:
|
DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2
|
||||||
Enabled: true
|
DefaultLogoutURLV2: "/ui/v2/login/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
|
||||||
|
SAML.DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
|
||||||
|
|
||||||
|
# Access logs allow us to debug Network issues
|
||||||
|
LogStore.Access.Stdout.Enabled: true
|
||||||
|
|
||||||
|
# Skipping the MFA init step allows us to immediately authenticate at the console
|
||||||
|
DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s"
|
||||||
|
@@ -1,8 +1,12 @@
|
|||||||
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml
|
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml
|
||||||
FirstInstance:
|
FirstInstance:
|
||||||
|
PatPath: '/pat'
|
||||||
Org:
|
Org:
|
||||||
Name: 'My Org'
|
# We want to authenticate immediately at the console without changing the password
|
||||||
Human:
|
Human:
|
||||||
# use the loginname root@my-org.my.domain
|
PasswordChangeRequired: false
|
||||||
Username: 'root'
|
Machine:
|
||||||
Password: 'RootPassword1!'
|
Machine:
|
||||||
|
Username: 'login-container'
|
||||||
|
Name: 'Login Container'
|
||||||
|
Pat.ExpirationDate: '2029-01-01T00:00:00Z'
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: A ZITADEL Load Balancing Example
|
title: A Zitadel Load Balancing Example
|
||||||
---
|
---
|
||||||
|
|
||||||
import CodeBlock from '@theme/CodeBlock';
|
import CodeBlock from '@theme/CodeBlock';
|
||||||
@@ -8,16 +8,16 @@ import ExampleTraefikSource from '!!raw-loader!./example-traefik.yaml'
|
|||||||
import ExampleZITADELConfigSource from '!!raw-loader!./example-zitadel-config.yaml'
|
import ExampleZITADELConfigSource from '!!raw-loader!./example-zitadel-config.yaml'
|
||||||
import ExampleZITADELSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml'
|
import ExampleZITADELSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml'
|
||||||
import ExampleZITADELInitStepsSource from '!!raw-loader!./example-zitadel-init-steps.yaml'
|
import ExampleZITADELInitStepsSource from '!!raw-loader!./example-zitadel-init-steps.yaml'
|
||||||
import NoteInstanceNotFound from '../troubleshooting/_note_instance_not_found.mdx';
|
|
||||||
|
|
||||||
With this example configuration, you create a near production environment for ZITADEL with [Docker Compose](https://docs.docker.com/compose/).
|
The stack consists of four long-running containers and a couple of short-lived containers:
|
||||||
|
- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy container with upstream HTTP/2 enabled, issuing a self-signed TLS certificate.
|
||||||
The stack consists of three long-running containers:
|
- A Login container that is accessible via Traefik at `/ui/v2/login`
|
||||||
- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy with upstream HTTP/2 enabled, issuing a self-signed TLS certificate.
|
- A Zitadel container that is accessible via Traefik at all other paths than `/ui/v2/login`.
|
||||||
- A secure ZITADEL container configured for a custom domain. As we terminate TLS with Traefik, we configure ZITADEL for `--tlsMode external`.
|
|
||||||
- An insecure [PostgreSQL](https://www.postgresql.org/docs/current/index.html).
|
- An insecure [PostgreSQL](https://www.postgresql.org/docs/current/index.html).
|
||||||
|
|
||||||
The setup is tested against Docker version 20.10.17 and Docker Compose version v2.2.3
|
The Traefik container and the login container call the Zitadel container via the internal Docker network at `h2c://zitadel:8080`
|
||||||
|
|
||||||
|
The setup is tested against Docker version 28.0.4 and Docker Compose version v2.34.0
|
||||||
|
|
||||||
By executing the commands below, you will download the following files:
|
By executing the commands below, you will download the following files:
|
||||||
|
|
||||||
@@ -64,22 +64,11 @@ tr -dc A-Za-z0-9 </dev/urandom | head -c 32 > ./zitadel-masterkey
|
|||||||
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"
|
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"
|
||||||
|
|
||||||
# Run the database and application containers
|
# Run the database and application containers
|
||||||
docker compose up --detach
|
docker compose up --detach --wait
|
||||||
```
|
```
|
||||||
|
|
||||||
Make `127.0.0.1` available at `my.domain`. For example, this can be achieved with an entry `127.0.0.1 my.domain` in the `/etc/hosts` file.
|
Open your favorite internet browser at https://127.0.0.1.sslip.io/ui/console?login_hint=zitadel-admin@zitadel.127.0.0.1.sslip.io.
|
||||||
|
Your browser warns you about the insecure self-signed TLS certificate. As 127.0.0.1.sslip.io resolves to your localhost, you can safely proceed.
|
||||||
Open your favorite internet browser at [https://my.domain/ui/console/](https://my.domain/ui/console/).
|
Use the password *Password1!* to log in.
|
||||||
You can safely proceed, if your browser warns you about the insecure self-signed TLS certificate.
|
|
||||||
This is the IAM admin users login according to your configuration in the [example-zitadel-init-steps.yaml](./example-zitadel-init-steps.yaml):
|
|
||||||
- **username**: *root@<span></span>my-org.my.domain*
|
|
||||||
- **password**: *RootPassword1!*
|
|
||||||
|
|
||||||
Read more about [the login process](/guides/integrate/login/oidc/login-users).
|
Read more about [the login process](/guides/integrate/login/oidc/login-users).
|
||||||
|
|
||||||
<NoteInstanceNotFound/>
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
You can connect to the database like this: `docker exec -it loadbalancing-example-db-1 psql --host localhost`
|
|
||||||
For example, to show all login names: `docker exec -it loadbalancing-example-db-1 psql -d zitadel --host localhost -c 'select * from projections.login_names3'`
|
|
||||||
|
Reference in New Issue
Block a user