Recreate a Databricks workspace's existing IP access list as a context-based ingress (CBI) policy via a single, focused CLI: dbx-migrate-ip-acls.
π‘ Looking for traffic-analysis-based ingress/egress policy generation? Those live in the sibling tool - databricks-network-policy-helper (
ingress/egress).
A network policy is a security-enforcing control. This tool recreates your current IP access
list as-is; it does not judge whether that ACL is correct or complete. You are responsible for
reviewing every rule (and the disabled-rule notice) before applying β an incorrect or incomplete
allow-list can block legitimate users (in enforce mode) or fail to block malicious ones. Trial with
--policy-mode dry_run first if unsure.
uv sync
# Propose-only (writes nothing): read the IP ACL and preview the CBI policy
uv run dbx-migrate-ip-acls --profile my-workspace --account-id <acct-id> \
--no-create-policy --no-auto-assign
# Migrate for real: create the policy (enforce) and bind this workspace to it
uv run dbx-migrate-ip-acls --profile my-workspace --account-id <acct-id>Or install the CLI on your PATH:
uv tool install .
dbx-migrate-ip-acls --helpAuth is the Databricks SDK's unified auth (--profile, DATABRICKS_* env, or OAuth).
Account-admin credentials are always required β the pre-checks and create/assign are all
account-level (see Account access below).
At a high level: the CLI reads the workspace's IP access lists through a workspace client, and does every check + write through an account-admin client against the Databricks account APIs. The engine turns the ACL into a CBI policy dataclass β carrying over the workspace's current egress verbatim β which is previewed, optionally exported (JSON + Terraform), and β once you confirm β created and bound to the workspace.
flowchart LR
U(["You"]) --> CLI["<b>dbx-migrate-ip-acls</b><br/>Typer CLI Β· review gates"]
subgraph TOOL["dbx_migrate_ip_acls"]
direction TB
CLI --> AUTH["auth Β· unified auth<br/>(profile / env / OAuth)"]
AUTH --> WSC["Workspace client"]
AUTH --> ACC["Account client<br/>(account admin)"]
CLI --> ENG["acl.py β analyze β build<br/>policy.py β CBI SDK builders"]
ENG --> EXP["export<br/>JSON + Terraform"]
end
WSC -->|"read IP access lists +<br/>enableIpAccessLists toggle"| WAPI["Databricks<br/>Workspace API"]
ENG -. builds policy .-> ACC
ACC -->|"pre-checks: PrivateLink /<br/>assigned policy Β· create + assign"| AAPI["Databricks<br/>Account API"]
EXP -. writes .-> FILES[["<policy-id>.json<br/><policy-id>.tf"]]
classDef you fill:#e2e3e5,stroke:#6c757d,color:#111
classDef cli fill:#d1e7dd,stroke:#146c43,color:#111
classDef comp fill:#cfe2ff,stroke:#0d6efd,color:#111
classDef ext fill:#fff3cd,stroke:#997404,color:#111
classDef file fill:#f8f9fa,stroke:#6c757d,color:#111
class U you
class CLI cli
class AUTH,WSC,ACC,ENG,EXP comp
class WAPI,AAPI ext
class FILES file
flowchart TD
A(["dbx-migrate-ip-acls"]) --> B{"Confirm target workspace?"}
B -->|no| X1["Abort β nothing written"]
B -->|yes| ACCT["Account access (prompt account_id)<br/>run pre-checks before reading the IP ACLs"]
ACCT --> PAS{"PrivateLink? (non-Azure only: workspace PAS<br/>attached or account VPC endpoints > 0)"}
PAS -->|yes| X2["ABORT β not supported yet"]
PAS -->|"no / Azure (checks skipped)"| AS0{"Will create AND assign?"}
AS0 -->|"yes: existing ENFORCED CBI policy"| X3["ABORT"]
AS0 -->|"yes: existing DRY-RUN CBI policy"| PROM["Warn; offer to promote to enforced, then stop"]
AS0 -->|"yes: none / allow-all"| GATE{"enableIpAccessLists Γ rule count<br/>(read IP access lists)"}
AS0 -->|"no: propose-only"| GATE
GATE -->|"enabled + 0 rules"| X4["No rules β nothing to migrate, stop"]
GATE -->|"disabled + 0 rules"| X4
GATE -->|"disabled + rules β enable & continue"| REEN["Set enableIpAccessLists=true β continue"]
GATE -->|"disabled + rules β decline / --yes"| X6["Not active β nothing to migrate, stop"]
REEN --> NAME
GATE -->|"enabled + 1+ rules"| NAME["Resolve policy name (prompt; blank = profile)<br/>must be unique β re-prompt if it exists"]
NAME --> RD["ALLOW β allow, BLOCK β deny (IPv4, ENABLED only)<br/>labels verbatim; disabled lists flagged, not migrated"]
RD --> P["Preview proposed policy + disabled-rule notice"]
P --> EXP{"--export?"}
EXP -->|yes| EXPW["Write JSON + Terraform"]
EXP -->|no| CR{"--create-policy? (default on)"}
EXPW --> CR
CR -->|"no (--no-create-policy)"| X5["Propose-only β nothing written"]
CR -->|yes| WMODE{"--policy-mode"}
WMODE -->|enforce| WE["Create ingress (blocking)<br/>+ egress copied from current policy"]
WMODE -->|dry_run| WD["Create ingress_dry_run (log-only)<br/>+ egress copied from current policy"]
WE --> AS{"--auto-assign? (default on)"}
WD --> AS
AS -->|no| DONE(["Done"])
AS -->|yes| ASB["Bind workspace to policy"]
ASB --> DIS{"--disable-existing-ip-acls?"}
DIS -->|no| DONE
DIS -->|yes| DISB["Set enableIpAccessLists=false"]
DISB --> DONE
classDef stop fill:#f8d7da,stroke:#b02a37,color:#111
classDef done fill:#e2e3e5,stroke:#6c757d,color:#111
classDef write fill:#d1e7dd,stroke:#146c43,color:#111
classDef warn fill:#fff3cd,stroke:#997404,color:#111
class X2,X3 stop
class X1,X4,X5,X6,DONE done
class WD,WE,ASB,DISB write
class PROM,REEN warn
- Right after the workspace is chosen β and before the IP access lists are read or shown β
runs account-level pre-checks, so an unsupported or already-migrated workspace fails fast. It
prompts for the
account_idif it wasn't passed, then aborts on PrivateLink β this workspace has a PAS attached, or the account has β₯1 registered VPC endpoint (any workspace; CBI private access isn't GA yet, so any PrivateLink account is blocked for now) β not supported yet; both PrivateLink checks are skipped on Azure, which has neither concept, so an Azure workspace only migrates its IP ACLs; and, only when the run will assign the new policy, guards an existing restrictive CBI ingress policy already bound to the workspace (enforced β abort; dry-run β offer to promote it to enforced, then stop). An allow-all policy such as the account's baselinedefault-policyis ignored. - Then decides whether there's anything to migrate, from the workspace-wide
enableIpAccessListstoggle Γ the number of IP access lists:- disabled + 0 rules β nothing to migrate β exit.
- disabled + 1+ rules β the rules aren't in effect. It prints the current IP-ACL config and
(interactively) offers to enable them: yes β sets
enableIpAccessLists=trueand continues in the same run; no β exits.--yesnever auto-flips the toggle. - enabled + 0 rules β nothing to migrate β exit.
- enabled + 1+ rules β proceed. (Unreadable toggle β warn + proceed.)
- Reads the workspace's IP access lists (
w.ip_access_lists.list()). Individual lists that are disabled are flagged and NOT migrated β only enabled lists are; the disabled ones are called out in the final printout so you can vet them. Maps ALLOW β allow rules, BLOCK β deny rules (IPv4 only; CBI is IPv4-only), recreating each rule verbatim β the original ACL label, no prefix, no mode suffix. The one thing it adds: if the ACL has only BLOCK lists, a catch-all allow (all public IPs) is added, because CBI RESTRICTED_ACCESS is default-deny β without it a deny-only policy would block everything. - Names the new policy from
--policy-name(or prompts; blank = the profile name). It only creates new policies, so a name that already exists re-prompts (or aborts non-interactively).--create-policyis on by default (a review gate still confirms); with--auto-assign(default on) it binds the current workspace. - With
--disable-existing-ip-acls(off by default), after the policy is created and assigned, turns off the workspace's IP access list enforcement (enableIpAccessLists=false) so the old ACL and the new CBI policy don't both apply. The lists themselves are preserved (reversible).
This tool deliberately does not enrich or auto-allow Databricks' own control-plane IPs β it assumes the existing ACL is what you want. It recreates the ingress (the IP ACLs) and, for egress, copies the egress of the policy the workspace currently runs under verbatim β its enforcement mode, allowed internet (FQDN) + storage destinations, and blocked-internet lists β so egress posture is preserved rather than reset. The source is the workspace's assigned policy, or the account baseline
default-policywhen nothing is assigned; only when neither is readable does it fall back to a permissiveFULL_ACCESSegress. This is automatic β there is no egress flag.
| Option | Meaning |
|---|---|
--profile |
Databricks CLI/config profile. Prompted if omitted (never guessed). |
--policy-mode enforce|dry_run |
enforce (default) blocks non-matching source IPs once assigned; dry_run is log-only. |
--policy-name |
The new policy's id. If omitted you're prompted (blank there = the profile name, falling back to the workspace id). Normalised to a lowercase, --safe id, capped at 30 chars. |
--export <path> |
Write the proposed policy JSON and a sibling best-effort Terraform .tf (databricks_account_network_policy β review before terraform apply). A directory writes <policy-id>.{json,tf} inside it (use --export . for the current dir); missing parents are created. Works in propose-only mode too. |
--auto-assign / --no-auto-assign |
Bind the current workspace to the new policy (default on). |
--create-policy / --no-create-policy |
Master write switch. On by default (a review gate still confirms). For a propose-only run: --no-create-policy --no-auto-assign. |
--disable-existing-ip-acls |
After create and assign, turn off the workspace's IP access lists. Requires create + assign and --policy-mode enforce. Off by default. |
--account-id (+ account-admin creds) |
Always required β pre-checks and create/assign are account-level. |
--account-host, --account-profile |
Account API host (when unset, derived from the workspace's environment β AWS staging / GCP / Azure β falling back to the AWS prod console) / a dedicated profile for account-level calls. |
--yes, -y |
Non-interactive: skip the step-through pauses and the review/write gate. --yes will create + assign. |
Invalid flag combinations (rejected up front β before the profile prompt or any account call, so
they fail instantly): --no-create-policy with --auto-assign (nothing to bind);
--disable-existing-ip-acls without both create + assign, or with --policy-mode dry_run (both
would leave the workspace with no enforced ingress control).
Every run is account-level: the pre-checks (PrivateLink / existing assigned policy) read account
APIs, and create/assign write them. Pass --account-id <numeric id> with account-admin
credentials resolvable by unified auth for the account host (an account-admin service principal via
OAuth M2M is the recommended path). A workspace-only OAuth session cannot call the account API β
use --account-profile (or env) for account creds if your workspace profile can't.
The account host is chosen to match the workspace's environment: unless you pass --account-host,
it's derived from the workspace host (e.g. an AWS staging workspace β
accounts.staging.cloud.databricks.com; GCP β accounts.gcp.databricks.com; Azure β
accounts.azuredatabricks.net), falling back to the AWS prod console when it can't be determined. So
a non-prod workspace no longer fails against the wrong account API.
At startup the tool registers databricks-migrate-ip-acls/<version> as a Databricks SDK
user-agent extra, so it's appended to the User-Agent
header of every API call it makes. That lets platform-side logs attribute API traffic to this tool.
It adds only the tool name + version β no arguments, workspace data, or PII. Granularity is
workspace-level: cluster ids are redacted in the request logs, so per-DBU attribution isn't
possible. See usage.py.
| Path | What |
|---|---|
src/dbx_migrate_ip_acls/cli.py |
The Typer CLI (single command) + interactive gates. |
src/dbx_migrate_ip_acls/acl.py |
The migration engine + network-policy state queries. |
src/dbx_migrate_ip_acls/policy.py |
SDK dataclass builders + policy-id naming. |
src/dbx_migrate_ip_acls/terraform.py |
Best-effort Terraform (HCL) rendering of the proposed policy. |
src/dbx_migrate_ip_acls/usage.py |
Registers the tool name in the SDK User-Agent (usage tracking). |
src/dbx_migrate_ip_acls/{config,auth,console,render,tls}.py |
Config/validation, auth, Rich UI, presentation, OS-trust-store TLS. |
tests/ |
Offline unit tests (fakes/monkeypatch β no network). |
uv run pytest -q # tests (fully offline)
uv run pytest --cov=dbx_migrate_ip_acls --cov-report=term-missing # with coverage
uv run ruff check src/ tests/ # lint
uv run black --check src/ tests/ # style (run `uv run black src/ tests/` to format)Tests are fully offline (SDK clients and workspace/account reads are faked or monkeypatched).
CI (.github/workflows/ci.yml) runs ruff + black --check + pytest-with-coverage on every push
and PR across Python 3.10β3.12, and uploads coverage to Codecov (the badge above).
Releases are fully automated β there is no manual upload step:
- Bump
versioninpyproject.toml. - Tag and push:
git tag vX.Y.Z && git push origin vX.Y.Z.
The release.yml workflow then builds the sdist + wheel and publishes to PyPI via Trusted
Publishing (OIDC) β no stored token. One-time PyPI setup is required (Project β Publishing):
owner andyweaves, repo databricks-migrate-ip-acls, workflow release.yml, environment pypi.