At a glance
- Level
- Beginner
- Time
- 10 min
Prerequisites
- A Linux host with Docker Engine (and Compose v2) that can reach your database
- Outbound HTTPS (TCP 443) from that host to Kimo's regional bridge endpoint
- Administrator access to your database to create a read-only role
- Workspace admin rights in Kimo to create an enrollment token
You will end up with
A hardened Kimo Bridge container connected to Kimo over an outbound-only tunnel, serving one database in Bridge mode under a deny-by-default policy, verified end to end.
Connectors used
What do you need before you start?
Kimo Bridge is the small package that lets Kimo query a database on your infrastructure without copying it to the cloud. If you want the architectural background first, read Your Data, Your Rules or the short Kimo Bridge overview. This guide is the hands-on part.
| Requirement | Minimum | Notes |
|---|---|---|
| Host | Linux x86_64 or arm64, 1 vCPU, 1 GB RAM | A small VM in the same subnet as the database is ideal |
| Container runtime | Docker Engine with Compose v2 | Podman works with the same flags |
| Egress | TCP 443 to bridge.eu.getkimo.com or bridge.us.getkimo.com | Plus DNS. An HTTPS proxy is supported via HTTPS_PROXY |
| Database access | TCP from the host to the database port | Prefer a read replica |
| Kimo role | Workspace admin | Needed to create enrollment tokens |
| Inbound rules | None | The bridge never listens on a public interface |
Step 1: Create a read-only database role
Do this before anything else. The bridge enforces its own policy, but the database role is the control that holds even if every other layer failed. Grant the narrowest access that covers the tables your dashboards need — usually a reporting or analytics schema, not the whole database.
PostgreSQL
PostgreSQL can attach default_transaction_read_only and statement_timeout to a role with ALTER ROLE … SET, so every session the bridge opens starts read-only and no statement can run forever.2Source 2 · PostgreSQL DocumentationClient Connection Defaultspostgresql.org7Source 7 · PostgreSQL DocumentationALTER ROLEpostgresql.org
CREATE ROLE kimo_bridge LOGIN PASSWORD 'use-a-generated-secret';
ALTER ROLE kimo_bridge SET default_transaction_read_only = on;
ALTER ROLE kimo_bridge SET statement_timeout = '30s';
ALTER ROLE kimo_bridge SET idle_in_transaction_session_timeout = '60s';
ALTER ROLE kimo_bridge CONNECTION LIMIT 10;
GRANT CONNECT ON DATABASE app TO kimo_bridge;
GRANT USAGE ON SCHEMA analytics TO kimo_bridge;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO kimo_bridge;
ALTER DEFAULT PRIVILEGES IN SCHEMA analytics
GRANT SELECT ON TABLES TO kimo_bridge;If your tables use row-level security, make sure the bridge role is not a superuser, does not have BYPASSRLS and does not own the tables: all three bypass RLS policies.3Source 3 · PostgreSQL DocumentationRow Security Policiespostgresql.org The Postgres connector documentation covers SSL modes and replica settings in more detail.
MySQL and MariaDB
MySQL can require TLS for the account and cap its connections and queries per hour directly in CREATE USER.4Source 4 · MySQL 8.4 Reference ManualCREATE USER Statementdev.mysql.com A database-level SELECT grant applies to every table in that database, so use a dedicated reporting database or grant per table if the database also holds sensitive tables.5Source 5 · MySQL 8.4 Reference ManualGRANT Statementdev.mysql.com
CREATE USER 'kimo_bridge'@'10.20.0.%'
IDENTIFIED BY 'use-a-generated-secret'
REQUIRE SSL
WITH MAX_USER_CONNECTIONS 10 MAX_QUERIES_PER_HOUR 20000;
GRANT SELECT ON reporting.* TO 'kimo_bridge'@'10.20.0.%';
SHOW GRANTS FOR 'kimo_bridge'@'10.20.0.%';Step 2: Get an enrollment token
- Step 1:
Open the Bridge console
In Kimo, go to Bridge and choose New bridge. Give it a name that says where it runs, such as
fra-prod-01. - Step 2:
Choose a group
Bridges in the same group serve the same sources and share load. Start with one bridge; you can add a second later for high availability.
- Step 3:
Copy the token
Kimo shows a one-time enrollment token beginning with
kbt_. It is valid for 24 hours and for a single enrollment. Treat it like a password until it is used.
The token is not a long-term credential. On first start the bridge generates a private key on your host, uses the token once to obtain a short-lived client certificate for that key, and from then on authenticates with the certificate. The key never leaves the host. The security model explains the full chain.
Step 3: Write kimo-bridge.yaml
The configuration file declares your sources and the policy for each. Create a directory for it and for secrets on the host:
sudo mkdir -p /opt/kimo-bridge/secrets
cd /opt/kimo-bridge
# One file per secret, readable only by root and the container user
printf '%s' 'kbt_live_...' | sudo tee secrets/token >/dev/null
printf '%s' 'postgres://kimo_bridge:...@10.20.0.15:5432/app?sslmode=verify-full' \
| sudo tee secrets/crm_dsn >/dev/null
sudo chown -R 10001:10001 secrets && sudo chmod 400 secrets/*bridge:
name: fra-prod-01
group: prod-eu
region: eu
sources:
- id: crm_pg
type: postgres
dsn_file: /run/secrets/crm_dsn
mode: bridge # live pushdown; set to "cloud" to sync instead
cache_ttl: 5m # 0s disables result caching on Kimo's side
policy:
default: deny
tables:
analytics.accounts:
columns: [id, region, plan, mrr_cents, created_at, churned_at]
analytics.invoices:
columns: [id, account_id, amount_cents, currency, paid_at]
min_group_size: 5
limits:
max_rows: 50000
timeout: 30s
rate: 10/s
audit:
path: /var/lib/kimo-bridge/audit.jsonl
mirror_to_kimo: trueStart narrow. Allow the columns your first dashboard needs, ship it, then widen deliberately. Column names that are not allow-listed cannot appear in any query — Kimo's semantic layer simply will not offer them.
Step 4: Run the container
The quickest path is a single docker run. The flags below are the hardened defaults we recommend: a read-only root filesystem, all Linux capabilities dropped, no privilege escalation, a non-root user, and an automatic restart policy — all standard docker run options.1Source 1 · Docker Documentationdocker container rundocs.docker.com
docker run -d --name kimo-bridge \
--restart unless-stopped \
--read-only --cap-drop ALL \
--security-opt no-new-privileges \
--user 10001:10001 \
-e KIMO_BRIDGE_TOKEN_FILE=/run/secrets/token \
-e KIMO_BRIDGE_CONFIG=/etc/kimo-bridge/kimo-bridge.yaml \
-v /opt/kimo-bridge/kimo-bridge.yaml:/etc/kimo-bridge/kimo-bridge.yaml:ro \
-v /opt/kimo-bridge/secrets:/run/secrets:ro \
-v kimo-bridge-data:/var/lib/kimo-bridge \
-p 127.0.0.1:8080:8080 \
ghcr.io/getkimo/bridge:1.4.2The kimo-bridge-data volume holds the bridge's identity (its key and current certificate) and the local audit log; it is the only writable path. Port 8080 serves /healthz, /readyz and Prometheus /metrics, bound to localhost only.
With Docker Compose
For anything you will keep, prefer Compose. Docker recommends secrets over environment variables for passwords and keys, because environment variables are often available to all processes and can end up in logs; Compose mounts each secret as a file under /run/secrets/.6Source 6 · Docker DocumentationManage secrets securely in Docker Composedocs.docker.com
services:
bridge:
image: ghcr.io/getkimo/bridge:1.4.2
restart: unless-stopped
read_only: true
user: '10001:10001'
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
environment:
KIMO_BRIDGE_TOKEN_FILE: /run/secrets/token
KIMO_BRIDGE_CONFIG: /etc/kimo-bridge/kimo-bridge.yaml
KIMO_BRIDGE_LOG_LEVEL: info
volumes:
- ./kimo-bridge.yaml:/etc/kimo-bridge/kimo-bridge.yaml:ro
- data:/var/lib/kimo-bridge
secrets: [token, crm_dsn]
ports:
- '127.0.0.1:8080:8080'
secrets:
token:
file: ./secrets/token
crm_dsn:
file: ./secrets/crm_dsn
volumes:
data: {}cd /opt/kimo-bridge && docker compose up -d
docker compose logs -f bridgeA healthy first start logs four lines in order: generated identity key, enrolled as fra-prod-01 in group prod-eu, tunnel established region=eu tls=1.3, and source crm_pg ready mode=bridge. Once enrolled, delete the token file; the bridge no longer needs it.
Behind an HTTPS proxy
If outbound traffic must go through a corporate proxy, add -e HTTPS_PROXY=http://proxy.internal:3128 (and NO_PROXY for your database host). The proxy must allow CONNECT to the bridge hostname on port 443 without terminating TLS — the bridge authenticates Kimo end to end and will refuse a re-signed certificate.
Without Docker: the static binary
Hosts that cannot run containers can use the kimo-bridge binary, published for Linux amd64 and arm64 alongside a SHA-256 checksum file. It reads the same configuration and secrets, so everything above applies unchanged. Run it under a dedicated system user with a hardened systemd unit:
sudo useradd --system --home /var/lib/kimo-bridge --shell /usr/sbin/nologin kimo-bridge
sudo install -m 0755 kimo-bridge /usr/local/bin/kimo-bridge
sudo tee /etc/systemd/system/kimo-bridge.service >/dev/null <<'UNIT'
[Unit]
Description=Kimo Bridge
After=network-online.target
Wants=network-online.target
[Service]
User=kimo-bridge
Environment=KIMO_BRIDGE_CONFIG=/opt/kimo-bridge/kimo-bridge.yaml
Environment=KIMO_BRIDGE_TOKEN_FILE=/opt/kimo-bridge/secrets/token
ExecStart=/usr/local/bin/kimo-bridge run
Restart=on-failure
StateDirectory=kimo-bridge
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
CapabilityBoundingSet=
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload && sudo systemctl enable --now kimo-bridgeWith the binary, the secret paths in kimo-bridge.yaml point at /opt/kimo-bridge/secrets/… instead of /run/secrets/…, and the files must be owned by the kimo-bridge user. Every command in the next step works the same way without the docker exec kimo-bridge prefix.
Step 5: Verify the installation
Run the built-in diagnostics inside the container. doctor checks DNS, egress, certificate validity, the tunnel, each source connection and the policy file.
docker exec kimo-bridge kimo-bridge doctor
docker exec kimo-bridge kimo-bridge policy check
docker exec kimo-bridge kimo-bridge test-source crm_pg \
--sql "select plan, count(*) from analytics.accounts group by 1"ok dns bridge.eu.getkimo.com resolved
ok egress 443/tcp reachable (proxy: none)
ok identity certificate valid, renews in 21h
ok tunnel connected, TLS 1.3, mutual auth
ok source crm_pg postgres 16.4, role read-only, statement_timeout=30s
ok policy 2 tables, 11 columns allowed, default denyVerification checklist
- The bridge shows Connected in the Bridge console, with the expected name and group.
- The source
crm_pgappears under Connectors with the mode you chose. kimo-bridge test-sourcereturns rows for an allowed query and refuses a query on a column that is not allow-listed.- An attempted write (
delete from analytics.accounts) is rejected by the bridge and logged as denied. - The audit entries appear in both
/var/lib/kimo-bridge/audit.jsonland Activity. ss -tlnpon the host shows the bridge listening only on 127.0.0.1:8080.
Then build something real: open Explore, pick the bridged source, and chart accounts by plan. If you are setting up board reporting, the SaaS metrics template works on bridged sources unchanged.
Should this source use Bridge or Cloud mode?
Each source picks its own mode, and you can change it later by editing one line. Use Bridge (mode: bridge) for sensitive data and anything with residency commitments: Kimo pushes each query down and keeps only short-lived result caches. Use Cloud (mode: cloud) for event data and heavy exploratory work: the bridge syncs tables incrementally through the same outbound tunnel, and Kimo keeps history. The trade-offs are covered in Cloud, hybrid or bridge.
Upgrade, rotate and revoke
| Task | Command or action | Effect |
|---|---|---|
| Upgrade | docker compose pull && docker compose up -d | New image, same identity; tunnel reconnects in seconds |
| Reload policy | docker exec kimo-bridge kimo-bridge reload | Applies kimo-bridge.yaml changes without dropping the tunnel |
| Rotate DB password | Update secrets/crm_dsn, then reload | New connections use the new secret |
| Pause | docker exec kimo-bridge kimo-bridge pause | Closes the tunnel and cancels in-flight queries until resume |
| Revoke | Bridge console → Revoke | Certificate revoked; bridge cannot reconnect; Kimo caches purged |
| Remove | docker compose down -v | Deletes the container, identity and local audit log |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
enrollment failed: token expired or already used | Token older than 24 hours, or a previous start already enrolled | Create a new token; if the volume holds an identity, the bridge does not need one |
tunnel: dial tcp ...:443: i/o timeout | Egress blocked or a proxy is required | Allow TCP 443 to the regional hostname, or set HTTPS_PROXY |
tls: certificate signed by unknown authority | A TLS-inspecting proxy is re-signing traffic | Exempt the bridge hostname from inspection; mutual TLS cannot work through interception |
region mismatch: workspace is us | Config says region: eu for a US workspace | Set the region that matches your workspace |
source crm_pg: permission denied for schema analytics | Missing USAGE on the schema | Grant USAGE as in Step 1 |
Queries return denied: column not allowed | Policy works as designed | Add the column to the allow-list if it is genuinely needed, then reload |
read-only file system at start | Data volume missing with --read-only | Mount a volume at /var/lib/kimo-bridge |
| Dashboards slow during business hours | Bridge points at the primary database | Point the DSN at a read replica; lower rate; consider mode: cloud for heavy tables |
Still stuck? Run kimo-bridge doctor --bundle to produce a redacted diagnostics archive (no credentials, no query results) and attach it to a support request from the contact page.
Sources
7 references- docker container run (opens in a new tab)Docker Documentationdocs.docker.com
Options --read-only, --cap-drop, --security-opt, --restart and --user.
- Client Connection Defaults (opens in a new tab)PostgreSQL Documentationpostgresql.org
statement_timeout, default_transaction_read_only and idle_in_transaction_session_timeout.
- Row Security Policies (opens in a new tab)PostgreSQL Documentationpostgresql.org
Superusers, BYPASSRLS roles and table owners bypass row security.
- CREATE USER Statement (opens in a new tab)MySQL 8.4 Reference Manualdev.mysql.com
REQUIRE SSL and per-account resource limits.
- GRANT Statement (opens in a new tab)MySQL 8.4 Reference Manualdev.mysql.com
Database-level privileges apply to all objects in the database.
- Manage secrets securely in Docker Compose (opens in a new tab)Docker Documentationdocs.docker.com
Secrets mounted under /run/secrets; environment variables discouraged for sensitive values.
- ALTER ROLE (opens in a new tab)PostgreSQL Documentationpostgresql.org
Role-specific session defaults (ALTER ROLE … SET) and CONNECTION LIMIT.
External sources were accessed at the time of writing. Kimo product details, customers and figures in examples are illustrative unless a source is cited.
Mark as done
0 of 9 sections done
Frequently asked questions
Do I have to open any inbound ports?
No. The bridge makes one outbound TLS connection to Kimo on TCP 443. The only port it listens on is the local health and metrics port, which you bind to 127.0.0.1.
Can I pass the token and DSN as environment variables?
Yes, KIMO_BRIDGE_TOKEN and plain dsn values work, but we recommend files mounted as secrets. Environment variables are visible to every process in the container and tend to end up in logs and support bundles.
How much load does the bridge put on my database?
Only what your dashboards ask for, bounded by the rate limit in the policy and the statement timeout on the role. Because aggregations are pushed down, typical dashboard queries are short. Point the bridge at a read replica for production.
What happens if the host restarts?
With restart set to unless-stopped and the data volume in place, the bridge restarts, reuses its identity and reconnects without a new token. For no-downtime restarts, run a second bridge in the same group.
Can one bridge serve several databases?
Yes. Add more entries under sources, each with its own DSN, mode and policy. Use separate bridges only when databases sit in network segments that should not be connected.
Skip the setup — start from a working version.
Open the app and follow along with the steps in this guide.





