Skip to content

Repository files navigation

πŸ” Databricks Migrate IP ACLs

CI codecov

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).

⚠️ Warning

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.

πŸš€ Quick start

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 --help

Auth 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).

πŸ—οΈ Architecture

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[["&lt;policy-id&gt;.json<br/>&lt;policy-id&gt;.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
Loading

πŸ—ΊοΈ How it flows

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
Loading

🧰 What it does

  1. 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_id if 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 baseline default-policy is ignored.
  2. Then decides whether there's anything to migrate, from the workspace-wide enableIpAccessLists toggle Γ— 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=true and continues in the same run; no β†’ exits. --yes never auto-flips the toggle.
    • enabled + 0 rules β†’ nothing to migrate β†’ exit.
    • enabled + 1+ rules β†’ proceed. (Unreadable toggle β†’ warn + proceed.)
  3. 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.
  4. 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-policy is on by default (a review gate still confirms); with --auto-assign (default on) it binds the current workspace.
  5. 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-policy when nothing is assigned; only when neither is readable does it fall back to a permissive FULL_ACCESS egress. This is automatic β€” there is no egress flag.

βš™οΈ Options

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).

πŸ”’ Account access

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.

πŸ“ˆ Usage tracking

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.

πŸ—‚οΈ Repo layout

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).

πŸ§ͺ Development & tests

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).

πŸ“¦ Releasing

Releases are fully automated β€” there is no manual upload step:

  1. Bump version in pyproject.toml.
  2. 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages