From c5a226c49954400eb1eb8ca0093cc06a11d7b646 Mon Sep 17 00:00:00 2001 From: eli Date: Wed, 8 Jul 2026 14:44:09 -0500 Subject: [PATCH 1/4] PER-14389: add "Migrate from Permit Cloud" on-prem import guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customer-facing guide for importing a Permit-delivered migration data package into an on-prem deployment: package request/verification, DB backup, write-freeze, ordered table import (with the same-session FK-disable form for api_key/pdp_config), post-import cleanup and superuser grant, org-filtered verification against the manifest, Keycloak email-linking for member access, and troubleshooting. Import side only — the export is performed by Permit and delivered as a secure download link; no internal tooling is referenced. Co-Authored-By: Claude Fable 5 --- .../deploy/on-prem/migrate-from-cloud.mdx | 355 ++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 docs/how-to/deploy/on-prem/migrate-from-cloud.mdx diff --git a/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx b/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx new file mode 100644 index 00000000..173f448b --- /dev/null +++ b/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx @@ -0,0 +1,355 @@ +--- +title: "Migrate from Permit Cloud" +description: "Import your Permit Cloud data into your on-premises deployment" +--- + +:::info Enterprise Only +This section is only relevant to Enterprise customers who acquired an on-prem license. +::: + +# Migrate from Permit Cloud to On-Premises + +This guide explains how to import your existing Permit Cloud (SaaS) data into your on-premises +Permit Platform deployment, so you can switch environments without rebuilding your authorization +model from scratch. + +Migration is a coordinated process: the Permit team exports your organization's data from Permit +Cloud and delivers it to you as a **migration data package**. You then import that package into +your own deployment using the steps below. + +## Requesting Your Migration Package + +Contact your Permit account team to schedule the migration. You will agree on: + +- **A migration window** - plan a short change freeze on your Permit Cloud workspace right before + the export, so the package captures your final state +- **Audit log history** (optional) - historical audit logs are not part of the standard package. + If you want them, request them when scheduling: Permit Cloud retains audit logs for **90 days**, + so history that is not exported during the migration window cannot be recovered later +- **Version alignment** - Permit confirms that your on-prem installer version matches the data + package (the package's `manifest.json` records the source schema version) + +Permit then performs the export and sends you a **secure, time-limited download link** to the +package. Download it promptly - the link expires after a few days. + +## What Gets Migrated + +The migration data package contains a complete snapshot of your organization's data: + +- **Authorization model** - resources, actions, roles, permissions, relations, condition sets +- **Directory data** - users, tenants, role assignments, relationship tuples, resource instances +- **Configuration** - PDP configurations, webhooks, proxy configs, Permit Elements, email templates +- **Workspace members** - your team members and their access levels +- **API keys** - your existing `permit_key_*` tokens migrate as-is, so your applications keep + working without code changes after you point them at the on-prem endpoint + +**What is not migrated:** + +- **Login credentials** - on-prem uses its own identity provider (Keycloak). Team members sign in + again with the **same email address** they used in Permit Cloud and automatically regain their + workspace access (see [Team member access](#team-member-access-after-import)) +- **Audit log history** - optional, by request only (see + [Requesting Your Migration Package](#requesting-your-migration-package)). New audit logs start + flowing as soon as your on-prem deployment is running +- **Runtime state** - PDPs re-register automatically, and generated policy is rebuilt from the + imported data on startup + +:::caution Handle the package securely +The migration data package contains your API keys. Store it securely, restrict access to it, and +delete all copies once the import is verified. +::: + +## Package Contents + +``` +permit-migration--/ +├── data/ +│ ├── v2_organization.csv +│ ├── ... (one CSV per table) +│ └── v2_identity.csv # reference only - do NOT import (see below) +├── manifest.json # row counts + sha256 checksums for verification +├── policy-repo/ # only if you used the default Permit-managed policy repo +└── README.md +``` + +:::warning Do not import `v2_identity.csv` +This file is a reference snapshot of your previous cloud login identities. It is intentionally +excluded from the import steps below - importing it can prevent team members from regaining +access when they first sign in to your on-prem deployment. +::: + +### Policy repository + +Your policy-as-code (Rego) lives in a Git repository, not in the database: + +- **If you connected your own Git repository in Permit Cloud (GitOps)** - keep using it. Point + your on-prem deployment at the same repository in `values.yaml` +- **If you used the default Permit-managed repository** - your package includes a `policy-repo/` + clone. Push it to a Git server you control and point your on-prem deployment at it, as + described in the package `README.md` + +Any custom Rego you wrote exists **only** in the Git repository - it is not part of the database +import, so make sure the repository is connected before you rely on custom policies. + +## Prerequisites + +- A healthy on-premises Permit Platform deployment + (see the [Installation Guide](./installation.mdx)) - all pods `Running`, migrations job + `Completed` +- The installer version confirmed by your Permit contact to match your migration data package +- Access to the Kubernetes cluster with `kubectl` configured +- The migration data package downloaded from the secure link provided by Permit + +## Step-by-Step Import + +### 1. Verify the package integrity + +Extract the package and verify every file against the manifest checksums: + +```bash +tar -xzf permit-migration--.tar.gz +cd permit-migration--/ + +# Verify checksums (on macOS use: shasum -a 256 -c -) +jq -r '.tables[] | "\(.sha256) \(.file)"' manifest.json | sha256sum -c - +``` + +Every line should print `OK`. If any file fails verification, re-download the package before +continuing. + +### 2. Stop services that write to the database + +Pause the platform services that write to PostgreSQL, so nothing changes mid-import: + +```bash +kubectl scale deployment -n permit-platform \ + permit-backend-v2 celery-general permit-dl-enricher-v2 --replicas=0 +``` + +### 3. Back up the database + +Take a backup before importing, so you can roll back cleanly if anything goes wrong: + +```bash +PG_POD=$(kubectl get pods -n permit-platform -l app=postgres \ + --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') + +kubectl exec -n permit-platform $PG_POD -- \ + pg_dump -U permit -d permit -F c -f /tmp/pre-migration.dump +kubectl cp permit-platform/$PG_POD:/tmp/pre-migration.dump ./pre-migration.dump +kubectl exec -n permit-platform $PG_POD -- rm -f /tmp/pre-migration.dump +``` + +If your PostgreSQL runs outside the cluster, use your own snapshot mechanism instead. + +### 4. Copy the data files to the PostgreSQL pod + +```bash +kubectl exec -n permit-platform $PG_POD -- mkdir -p /tmp/import +kubectl cp data permit-platform/$PG_POD:/tmp/import/data +``` + +### 5. Import the data tables + +Import the tables in dependency order. Note that `v2_identity` is **not** in this list - +that is intentional. The loop stops on the first failure instead of continuing with a partial +import; psql prints `COPY ` for each successful table. + +```bash +for TABLE in v2_organization v2_member v2_project v2_environment \ + v2_resource v2_resource_action v2_resource_action_group \ + v2_resource_action_group_association v2_resource_attribute \ + v2_role v2_role_permission v2_role_hierarchy \ + v2_role_derivations v2_role_derivation_rules \ + v2_relation v2_condition_set v2_condition_set_rule \ + v2_implicit_role_grant v2_group_role_permission \ + v2_tenant v2_user v2_user_tenant_association \ + v2_resource_instance v2_relationship_tuple v2_monthly_active_user \ + v2_webhook v2_proxy_config \ + v2_elements_config v2_elements_role_permission \ + v2_elements_user_invite v2_email_configuration v2_email_template \ + v2_sso_connection v2_policy_repo v2_scope_config \ + v2_policy_guard_rule v2_policy_guard_scope \ + v2_policy_guard_scope_detail v2_member_access \ + v2_access_requests v2_user_invite; do + kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c \ + "\COPY v2.${TABLE} FROM '/tmp/import/data/${TABLE}.csv' WITH CSV HEADER" \ + && echo "imported: $TABLE" \ + || { echo "FAILED on $TABLE - stop here and see Troubleshooting"; break; } +done +``` + +### 6. Import the API key and PDP configuration tables + +`v2_api_key` can reference workspace members who are no longer part of your organization, and +`v2_pdp_config` references those API keys - so both are imported with foreign-key checks +temporarily disabled. The `SET` and the `\COPY` **must run in the same psql session**, which is +why the commands are piped together: + +```bash +for TABLE in v2_api_key v2_pdp_config; do + (echo "SET session_replication_role = 'replica';" && \ + echo "\COPY v2.${TABLE} FROM '/tmp/import/data/${TABLE}.csv' WITH CSV HEADER") | \ + kubectl exec -i -n permit-platform $PG_POD -- psql -U permit -d permit \ + && echo "imported: $TABLE" \ + || { echo "FAILED on $TABLE - stop here and see Troubleshooting"; break; } +done +``` + +Then clear any references to members that are not part of the imported data (these come from +members who left your organization before the export): + +```bash +kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c " +UPDATE v2.v2_api_key k SET created_by_member_id = NULL +WHERE created_by_member_id IS NOT NULL +AND NOT EXISTS (SELECT 1 FROM v2.v2_member m WHERE m.id = k.created_by_member_id); +UPDATE v2.v2_user_invite ui SET member_id = NULL +WHERE member_id IS NOT NULL +AND NOT EXISTS (SELECT 1 FROM v2.v2_member m WHERE m.id = ui.member_id);" +``` + +### 7. Clear cloud-specific configuration + +Remove references that only apply to Permit Cloud: + +```bash +kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c " +UPDATE v2.v2_environment SET avp_policy_store_id = NULL +WHERE avp_policy_store_id IS NOT NULL;" +``` + +After the import, also review these imported settings - they may carry over values that only +made sense in Permit Cloud: + +- **SSO connections** - imported SSO settings reference the Permit Cloud login integration. + Reconfigure SSO against your on-prem Keycloak (or delete the stale entries) +- **Webhooks** - your on-prem deployment sends webhooks from a different source IP; update + firewall allowlists on the receiving side if needed +- **PDP configurations** - review any PDP settings that point at Permit Cloud endpoints + +### 8. Grant your admin access to the imported organization + +The admin account you created during installation is not a member of the imported organization +yet. Make it a superuser so it can see and manage all organizations, including the imported one: + +```bash +kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c " +UPDATE v2.v2_member SET is_superuser = true WHERE email = '';" +``` + +Log out and log back in for the change to take effect. + +### 9. Clean up and restart + +```bash +# Remove the import files from the pod +kubectl exec -n permit-platform $PG_POD -- rm -rf /tmp/import + +# Scale the services back up +kubectl scale deployment -n permit-platform \ + permit-backend-v2 celery-general permit-dl-enricher-v2 --replicas=1 +``` + +If you customized the replica counts in your `values.yaml`, scale back to your configured values +instead of `1`. + +## Verify the Import + +Compare the imported row counts against the values in `manifest.json`. The counts must be +filtered to your imported organization - your deployment may already contain other organizations +(for example, the one created by your installation admin): + +```bash +# Your organization id is recorded in the manifest +ORG_ID=$(jq -r '.org_id' manifest.json) + +kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c " +SELECT 'projects' AS table, count(*) FROM v2.v2_project WHERE org_id = '$ORG_ID' +UNION ALL SELECT 'environments', count(*) FROM v2.v2_environment WHERE org_id = '$ORG_ID' +UNION ALL SELECT 'users', count(*) FROM v2.v2_user WHERE org_id = '$ORG_ID' +UNION ALL SELECT 'tenants', count(*) FROM v2.v2_tenant WHERE org_id = '$ORG_ID' +UNION ALL SELECT 'roles', count(*) FROM v2.v2_role WHERE org_id = '$ORG_ID' +UNION ALL SELECT 'relationship_tuples', count(*) FROM v2.v2_relationship_tuple WHERE org_id = '$ORG_ID' +UNION ALL SELECT 'api_keys', count(*) FROM v2.v2_api_key WHERE org_id = '$ORG_ID';" +``` + +Then confirm in the UI: + +1. Log in to your on-prem dashboard as the admin - the imported organization should be visible +2. Open the **Members** page - your team members appear with their original access levels +3. Open the **Policy** page of a migrated environment - your resources, roles, and permissions + are present + +## Team Member Access After Import + +Your team members' profiles and permission levels are fully migrated. To sign in, each member +authenticates against your on-prem identity provider (Keycloak) **using the same email address +they used in Permit Cloud** - the platform automatically links them to their migrated profile and +restores their workspace access on first login. + +Depending on how you configured Keycloak, members either: + +- **Sign in through your corporate SSO** (if you federated your IdP with Keycloak), or +- **Self-register** on the on-prem login page with their work email + +:::caution Email verification is required +The automatic account linking only happens for verified email addresses. Make sure email +verification is enabled in your Keycloak realm (or that your federated IdP passes +`email_verified=true`). A member who registers with a different or unverified email will start +with a fresh account and no access. +::: + +## Point Your Applications at On-Prem + +Your API keys were migrated as-is, so the only change your applications need is the endpoint: + +```python +# Before (Permit Cloud) +permit = Permit( + pdp="https://cloudpdp.api.permit.io", + token="permit_key_XXXX", +) + +# After (On-Premises) - same key, new endpoint +permit = Permit( + pdp="https://", + token="permit_key_XXXX", +) +``` + +## Troubleshooting + +### A `\COPY` command fails partway through the import + +Each `\COPY` is atomic - a failed table imports zero rows, and the tables before it in the list +are already fully imported. Fix the cause (usually a wrong file path or an out-of-order run), +then resume the loop **from the table that failed**. Re-running an already-imported table fails +with duplicate key errors without changing any data. If you are unsure of the state, restore the +backup from step 3 and start over, or contact Permit support. + +### `SET session_replication_role` appears to have no effect + +Each `kubectl exec` opens a new database session, and the setting only applies to the current +session. Use the piped form shown in step 6 so the `SET` and the `\COPY` run in the same session. + +### The imported organization is not visible in the UI + +Complete step 8 (superuser grant), then log out and log back in. You can list all organizations +with: + +```bash +kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c \ + "SELECT id, name, key FROM v2.v2_organization;" +``` + +### A team member logs in and sees an empty workspace + +Their login email does not match their Permit Cloud email, or their email is not verified. Check +the email on the **Members** page, and verify your Keycloak realm has email verification enabled. + +## See Also + +- [Installation Guide](./installation.mdx) +- [Management Guide](./management.mdx) +- [Troubleshooting](./troubleshooting.mdx) From dc345be4a8207a24ec185622648f3ed9df02a083 Mon Sep 17 00:00:00 2001 From: eli Date: Wed, 8 Jul 2026 14:55:41 -0500 Subject: [PATCH 2/4] =?UTF-8?q?PER-14389:=20round-2=20review=20fixes=20?= =?UTF-8?q?=E2=80=94=20disclosure=20+=20customer-friendliness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: the SDK cutover snippet omitted the api_url override, so a migrated app's management calls would silently keep writing to the old Permit Cloud workspace; it also pointed pdp= at the platform domain instead of an on-prem PDP deployment. Now shows both endpoints with a do-not-skip caution and links PDP Deployment. Over-disclosure removed: - 41-table schema list replaced with a manifest-driven loop (manifest.import.tables_in_order / fk_disabled_tables — contract added to the export in the backend PR), so public docs carry no schema dump and can't drift per installer version - avp_policy_store_id cleanup SQL (cloud-provider internals) moved to the package README; docs keep only the customer-relevant review bullets - unilateral commitments softened: audit retention period, download-link expiry, "without code changes"/"automatically regain access" guarantees Friendliness: "At a glance" orientation (phases, downtime window, done criteria, Permit assistance), pg_restore rollback snippet, jq/tar/sha256 prerequisites, replica-count note before scale-down, FK-bypass scoping caution, platform-wide superuser caution, dependency-order gloss, and the standard Support footer. Build: 0 bad links, 0 bad anchors. Co-Authored-By: Claude Fable 5 --- .../deploy/on-prem/migrate-from-cloud.mdx | 148 +++++++++++------- 1 file changed, 94 insertions(+), 54 deletions(-) diff --git a/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx b/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx index 173f448b..db0dac96 100644 --- a/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx +++ b/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx @@ -17,6 +17,15 @@ Migration is a coordinated process: the Permit team exports your organization's Cloud and delivers it to you as a **migration data package**. You then import that package into your own deployment using the steps below. +:::info At a glance +The migration has five phases: **request the package** → **freeze & export** (done by Permit) → +**import** (steps 1-9 below) → **verify** → **point your applications at on-prem**. Your on-prem +platform is offline between steps 2 and 9 - for a typical organization the import itself takes +well under an hour, and your Permit account team is available to assist during the window. You +are done when the row counts match the manifest and the UI checks in +[Verify the Import](#verify-the-import) pass. +::: + ## Requesting Your Migration Package Contact your Permit account team to schedule the migration. You will agree on: @@ -24,13 +33,14 @@ Contact your Permit account team to schedule the migration. You will agree on: - **A migration window** - plan a short change freeze on your Permit Cloud workspace right before the export, so the package captures your final state - **Audit log history** (optional) - historical audit logs are not part of the standard package. - If you want them, request them when scheduling: Permit Cloud retains audit logs for **90 days**, - so history that is not exported during the migration window cannot be recovered later + Permit Cloud retains them for a limited period, so history that is not exported around the + migration window cannot be recovered later - if you want it, request it when scheduling and + confirm the retention window with your account team - **Version alignment** - Permit confirms that your on-prem installer version matches the data package (the package's `manifest.json` records the source schema version) Permit then performs the export and sends you a **secure, time-limited download link** to the -package. Download it promptly - the link expires after a few days. +package. Your Permit contact will tell you when the link expires - download it promptly. ## What Gets Migrated @@ -40,14 +50,16 @@ The migration data package contains a complete snapshot of your organization's d - **Directory data** - users, tenants, role assignments, relationship tuples, resource instances - **Configuration** - PDP configurations, webhooks, proxy configs, Permit Elements, email templates - **Workspace members** - your team members and their access levels -- **API keys** - your existing `permit_key_*` tokens migrate as-is, so your applications keep - working without code changes after you point them at the on-prem endpoint +- **API keys** - your existing `permit_key_*` tokens migrate as-is; your applications keep using + the same keys and only the endpoints change (see + [Point Your Applications at On-Prem](#point-your-applications-at-on-prem)) **What is not migrated:** - **Login credentials** - on-prem uses its own identity provider (Keycloak). Team members sign in - again with the **same email address** they used in Permit Cloud and automatically regain their - workspace access (see [Team member access](#team-member-access-after-import)) + again and regain their workspace access on first login, provided they use the **same verified + email address** they used in Permit Cloud (see + [Team member access](#team-member-access-after-import)) - **Audit log history** - optional, by request only (see [Requesting Your Migration Package](#requesting-your-migration-package)). New audit logs start flowing as soon as your on-prem deployment is running @@ -64,10 +76,9 @@ delete all copies once the import is verified. ``` permit-migration--/ ├── data/ -│ ├── v2_organization.csv │ ├── ... (one CSV per table) │ └── v2_identity.csv # reference only - do NOT import (see below) -├── manifest.json # row counts + sha256 checksums for verification +├── manifest.json # row counts, checksums, and the import order ├── policy-repo/ # only if you used the default Permit-managed policy repo └── README.md ``` @@ -98,6 +109,8 @@ import, so make sure the repository is connected before you rely on custom polic `Completed` - The installer version confirmed by your Permit contact to match your migration data package - Access to the Kubernetes cluster with `kubectl` configured +- `jq`, `tar`, and `sha256sum` (macOS: `shasum`) available on the workstation where you extract + the package - The migration data package downloaded from the secure link provided by Permit ## Step-by-Step Import @@ -119,7 +132,14 @@ continuing. ### 2. Stop services that write to the database -Pause the platform services that write to PostgreSQL, so nothing changes mid-import: +First note the current replica counts, so you can restore them in step 9: + +```bash +kubectl get deployment -n permit-platform \ + permit-backend-v2 celery-general permit-dl-enricher-v2 +``` + +Then pause these services so nothing changes mid-import: ```bash kubectl scale deployment -n permit-platform \ @@ -142,6 +162,14 @@ kubectl exec -n permit-platform $PG_POD -- rm -f /tmp/pre-migration.dump If your PostgreSQL runs outside the cluster, use your own snapshot mechanism instead. +**To roll back later** (with the services still scaled to 0): + +```bash +kubectl cp ./pre-migration.dump permit-platform/$PG_POD:/tmp/pre-migration.dump +kubectl exec -n permit-platform $PG_POD -- \ + pg_restore -U permit -d permit --clean --if-exists /tmp/pre-migration.dump +``` + ### 4. Copy the data files to the PostgreSQL pod ```bash @@ -151,27 +179,15 @@ kubectl cp data permit-platform/$PG_POD:/tmp/import/data ### 5. Import the data tables -Import the tables in dependency order. Note that `v2_identity` is **not** in this list - -that is intentional. The loop stops on the first failure instead of continuing with a partial -import; psql prints `COPY ` for each successful table. +The manifest lists your tables in **dependency order** - tables that others reference are +imported first, so every reference already exists when it is needed. (`v2_identity` and two +tables handled in step 6 are deliberately not in this list.) + +The loop stops on the first failure instead of continuing with a partial import; psql prints +`COPY ` for each successful table: ```bash -for TABLE in v2_organization v2_member v2_project v2_environment \ - v2_resource v2_resource_action v2_resource_action_group \ - v2_resource_action_group_association v2_resource_attribute \ - v2_role v2_role_permission v2_role_hierarchy \ - v2_role_derivations v2_role_derivation_rules \ - v2_relation v2_condition_set v2_condition_set_rule \ - v2_implicit_role_grant v2_group_role_permission \ - v2_tenant v2_user v2_user_tenant_association \ - v2_resource_instance v2_relationship_tuple v2_monthly_active_user \ - v2_webhook v2_proxy_config \ - v2_elements_config v2_elements_role_permission \ - v2_elements_user_invite v2_email_configuration v2_email_template \ - v2_sso_connection v2_policy_repo v2_scope_config \ - v2_policy_guard_rule v2_policy_guard_scope \ - v2_policy_guard_scope_detail v2_member_access \ - v2_access_requests v2_user_invite; do +for TABLE in $(jq -r '.import.tables_in_order[]' manifest.json); do kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c \ "\COPY v2.${TABLE} FROM '/tmp/import/data/${TABLE}.csv' WITH CSV HEADER" \ && echo "imported: $TABLE" \ @@ -181,13 +197,13 @@ done ### 6. Import the API key and PDP configuration tables -`v2_api_key` can reference workspace members who are no longer part of your organization, and -`v2_pdp_config` references those API keys - so both are imported with foreign-key checks -temporarily disabled. The `SET` and the `\COPY` **must run in the same psql session**, which is -why the commands are piped together: +API keys can reference workspace members who left your organization before the export, and PDP +configurations reference those API keys - so these two tables are imported with foreign-key +checks temporarily disabled. The `SET` and the `\COPY` **must run in the same psql session**, +which is why the commands are piped together: ```bash -for TABLE in v2_api_key v2_pdp_config; do +for TABLE in $(jq -r '.import.fk_disabled_tables[]' manifest.json); do (echo "SET session_replication_role = 'replica';" && \ echo "\COPY v2.${TABLE} FROM '/tmp/import/data/${TABLE}.csv' WITH CSV HEADER") | \ kubectl exec -i -n permit-platform $PG_POD -- psql -U permit -d permit \ @@ -196,6 +212,12 @@ for TABLE in v2_api_key v2_pdp_config; do done ``` +:::caution Use this form only where shown +`session_replication_role = 'replica'` bypasses foreign-key checks and triggers for the session. +It is needed only for the tables listed in the manifest's `fk_disabled_tables`. If a table in +step 5 fails, fix the cause (see Troubleshooting) - do not disable checks for it. +::: + Then clear any references to members that are not part of the imported data (these come from members who left your organization before the export): @@ -209,18 +231,13 @@ WHERE member_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v2.v2_member m WHERE m.id = ui.member_id);" ``` -### 7. Clear cloud-specific configuration +### 7. Review cloud-specific configuration -Remove references that only apply to Permit Cloud: +Run the short cloud-reference cleanup included in your package (it removes settings that only +apply to Permit Cloud) - see the **Cleanup** section of the package `README.md`. -```bash -kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c " -UPDATE v2.v2_environment SET avp_policy_store_id = NULL -WHERE avp_policy_store_id IS NOT NULL;" -``` - -After the import, also review these imported settings - they may carry over values that only -made sense in Permit Cloud: +Afterwards, review these imported settings - they may carry over values that only made sense in +Permit Cloud: - **SSO connections** - imported SSO settings reference the Permit Cloud login integration. Reconfigure SSO against your on-prem Keycloak (or delete the stale entries) @@ -240,20 +257,22 @@ UPDATE v2.v2_member SET is_superuser = true WHERE email = '';" Log out and log back in for the change to take effect. +:::caution Superuser applies platform-wide +A superuser can see and manage **all** organizations on your deployment, not just the imported +one. Grant it only to your installation admin account. +::: + ### 9. Clean up and restart ```bash # Remove the import files from the pod kubectl exec -n permit-platform $PG_POD -- rm -rf /tmp/import -# Scale the services back up +# Scale the services back up (use the replica counts you noted in step 2) kubectl scale deployment -n permit-platform \ permit-backend-v2 celery-general permit-dl-enricher-v2 --replicas=1 ``` -If you customized the replica counts in your `values.yaml`, scale back to your configured values -instead of `1`. - ## Verify the Import Compare the imported row counts against the values in `manifest.json`. The counts must be @@ -265,7 +284,7 @@ filtered to your imported organization - your deployment may already contain oth ORG_ID=$(jq -r '.org_id' manifest.json) kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c " -SELECT 'projects' AS table, count(*) FROM v2.v2_project WHERE org_id = '$ORG_ID' +SELECT 'projects' AS table_name, count(*) FROM v2.v2_project WHERE org_id = '$ORG_ID' UNION ALL SELECT 'environments', count(*) FROM v2.v2_environment WHERE org_id = '$ORG_ID' UNION ALL SELECT 'users', count(*) FROM v2.v2_user WHERE org_id = '$ORG_ID' UNION ALL SELECT 'tenants', count(*) FROM v2.v2_tenant WHERE org_id = '$ORG_ID' @@ -302,7 +321,13 @@ with a fresh account and no access. ## Point Your Applications at On-Prem -Your API keys were migrated as-is, so the only change your applications need is the endpoint: +Your API keys were migrated as-is, so your applications keep using the same tokens - they just +need to talk to your on-prem deployment instead of Permit Cloud. Two endpoints change: + +1. **The PDP address** - deploy a PDP against your on-prem platform (see + [PDP Deployment](./pdp-deployment.mdx)) and point your application at it +2. **The API address** - point the SDK's API URL at your on-prem platform, so management calls + (like syncing users) reach your deployment and not Permit Cloud ```python # Before (Permit Cloud) @@ -311,13 +336,20 @@ permit = Permit( token="permit_key_XXXX", ) -# After (On-Premises) - same key, new endpoint +# After (On-Premises) - same key, new endpoints permit = Permit( - pdp="https://", + pdp="http://:7766", # the PDP you deployed on-prem + api_url="https:///api", # your on-prem Permit API token="permit_key_XXXX", ) ``` +:::caution Do not skip the API URL +If `api_url` still points at Permit Cloud, your migrated API keys remain valid there - your +application would silently keep writing users and permissions to your old cloud workspace +instead of your on-prem deployment. +::: + ## Troubleshooting ### A `\COPY` command fails partway through the import @@ -325,8 +357,8 @@ permit = Permit( Each `\COPY` is atomic - a failed table imports zero rows, and the tables before it in the list are already fully imported. Fix the cause (usually a wrong file path or an out-of-order run), then resume the loop **from the table that failed**. Re-running an already-imported table fails -with duplicate key errors without changing any data. If you are unsure of the state, restore the -backup from step 3 and start over, or contact Permit support. +with duplicate key errors without changing any data. If you are unsure of the state, roll back +using the backup from step 3 and start over, or contact Permit support. ### `SET session_replication_role` appears to have no effect @@ -348,8 +380,16 @@ kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c \ Their login email does not match their Permit Cloud email, or their email is not verified. Check the email on the **Members** page, and verify your Keycloak realm has email verification enabled. +## Support + +Need help with your migration? + +- 📧 **Email**: [support@permit.io](mailto:support@permit.io) +- 💬 **Slack**: [Join our community](https://io.permit.io/slack) + ## See Also - [Installation Guide](./installation.mdx) +- [PDP Deployment](./pdp-deployment.mdx) - [Management Guide](./management.mdx) - [Troubleshooting](./troubleshooting.mdx) From 702f53d57a2279ff6f17b807cfc8dd923a492ef8 Mon Sep 17 00:00:00 2001 From: eli Date: Wed, 8 Jul 2026 15:08:45 -0500 Subject: [PATCH 3/4] PER-14389: single-transaction import (FK-graph analysis fix) FK-graph verification against the schema found the documented ordered import cannot work for all customers: v2_project <-> v2_policy_repo reference each other (a genuine FK cycle, hit by any GitOps org), plus two more order violations masked in the original validation by empty tables. The import now runs as one FK-deferred transaction (--single-transaction + ON_ERROR_STOP): all-or-nothing, safe because the package is a single consistent snapshot, and simpler for customers - any failure rolls back cleanly and the step is just rerun. Troubleshooting updated accordingly (partial-import scenario no longer exists; added a guard entry for packages predating manifest.import). Co-Authored-By: Claude Fable 5 --- .../deploy/on-prem/migrate-from-cloud.mdx | 72 ++++++++----------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx b/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx index db0dac96..4a35abf6 100644 --- a/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx +++ b/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx @@ -177,48 +177,33 @@ kubectl exec -n permit-platform $PG_POD -- mkdir -p /tmp/import kubectl cp data permit-platform/$PG_POD:/tmp/import/data ``` -### 5. Import the data tables +### 5. Import the data -The manifest lists your tables in **dependency order** - tables that others reference are -imported first, so every reference already exists when it is needed. (`v2_identity` and two -tables handled in step 6 are deliberately not in this list.) - -The loop stops on the first failure instead of continuing with a partial import; psql prints -`COPY ` for each successful table: +The manifest lists every table to import (`v2_identity` is deliberately not in the list). The +whole import runs as **one database transaction**: if anything fails, nothing is imported - fix +the cause and simply run it again. psql prints `COPY ` for each table as it loads: ```bash -for TABLE in $(jq -r '.import.tables_in_order[]' manifest.json); do - kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c \ - "\COPY v2.${TABLE} FROM '/tmp/import/data/${TABLE}.csv' WITH CSV HEADER" \ - && echo "imported: $TABLE" \ - || { echo "FAILED on $TABLE - stop here and see Troubleshooting"; break; } -done +{ + echo '\set ON_ERROR_STOP on' + echo "SET session_replication_role = 'replica';" + jq -r '.import.tables_in_order[]' manifest.json | while read -r TABLE; do + echo "\\COPY v2.${TABLE} FROM '/tmp/import/data/${TABLE}.csv' WITH CSV HEADER" + done +} | kubectl exec -i -n permit-platform $PG_POD -- psql -U permit -d permit --single-transaction ``` -### 6. Import the API key and PDP configuration tables - -API keys can reference workspace members who left your organization before the export, and PDP -configurations reference those API keys - so these two tables are imported with foreign-key -checks temporarily disabled. The `SET` and the `\COPY` **must run in the same psql session**, -which is why the commands are piped together: - -```bash -for TABLE in $(jq -r '.import.fk_disabled_tables[]' manifest.json); do - (echo "SET session_replication_role = 'replica';" && \ - echo "\COPY v2.${TABLE} FROM '/tmp/import/data/${TABLE}.csv' WITH CSV HEADER") | \ - kubectl exec -i -n permit-platform $PG_POD -- psql -U permit -d permit \ - && echo "imported: $TABLE" \ - || { echo "FAILED on $TABLE - stop here and see Troubleshooting"; break; } -done -``` - -:::caution Use this form only where shown -`session_replication_role = 'replica'` bypasses foreign-key checks and triggers for the session. -It is needed only for the tables listed in the manifest's `fk_disabled_tables`. If a table in -step 5 fails, fix the cause (see Troubleshooting) - do not disable checks for it. +:::info Why referential checks are deferred +Some Permit tables reference each other in both directions, so no import order can satisfy +per-row referential checks - the import therefore defers them for this session +(`session_replication_role`). This is safe: your package was exported as a single consistent +database snapshot, so the data is guaranteed to be internally consistent. Use this technique +only for this import - not for ad-hoc database changes. ::: -Then clear any references to members that are not part of the imported data (these come from +### 6. Clear references to departed members + +Clear any references to members that are not part of the imported data (these come from members who left your organization before the export): ```bash @@ -352,18 +337,17 @@ instead of your on-prem deployment. ## Troubleshooting -### A `\COPY` command fails partway through the import +### The import fails partway through -Each `\COPY` is atomic - a failed table imports zero rows, and the tables before it in the list -are already fully imported. Fix the cause (usually a wrong file path or an out-of-order run), -then resume the loop **from the table that failed**. Re-running an already-imported table fails -with duplicate key errors without changing any data. If you are unsure of the state, roll back -using the backup from step 3 and start over, or contact Permit support. +The import runs in a single transaction, so a failure means **nothing was imported** - the +database is unchanged. Fix the cause shown in the error (usually a missing file or a wrong +path), then run step 5 again from the start. If you are unsure of the state, roll back using +the backup from step 3, or contact Permit support. -### `SET session_replication_role` appears to have no effect +### The import loop prints nothing -Each `kubectl exec` opens a new database session, and the setting only applies to the current -session. Use the piped form shown in step 6 so the `SET` and the `\COPY` run in the same session. +Your `manifest.json` predates the `import` section. Check the package `README.md` for the +ready-to-run import script matching your package version, or contact Permit support. ### The imported organization is not visible in the UI From 2c8ac070d0639ea6c900412bffba8725fbd46b3d Mon Sep 17 00:00:00 2001 From: eli Date: Wed, 8 Jul 2026 16:28:07 -0500 Subject: [PATCH 4/4] =?UTF-8?q?PER-14389:=20final=20proofread=20fixes=20?= =?UTF-8?q?=E2=80=94=20version-safe=20atomicity=20+=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace --single-transaction with explicit BEGIN/COMMIT: on psql <= 14 a client-side \COPY failure under --single-transaction COMMITs the already-loaded tables (rollback-on-client-error landed in psql 15), breaking the all-or-nothing promise; explicit BEGIN/COMMIT is atomic on every version (verified empirically on psql 14 and 16). - Admonition: "turned off" not "deferred" (no DEFERRABLE semantics), and forward-reference the step-6 member cleanup as the one known exception to snapshot consistency. - Step 8: log out/in only works after the step-9 restart. - Step 9: per-deployment scale commands (one --replicas flag can't express three different counts). - Troubleshooting: "prints no COPY lines" (psql still prints SET/BEGIN). Co-Authored-By: Claude Fable 5 --- .../deploy/on-prem/migrate-from-cloud.mdx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx b/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx index 4a35abf6..dd80def6 100644 --- a/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx +++ b/docs/how-to/deploy/on-prem/migrate-from-cloud.mdx @@ -186,19 +186,22 @@ the cause and simply run it again. psql prints `COPY ` for each table ```bash { echo '\set ON_ERROR_STOP on' + echo 'BEGIN;' echo "SET session_replication_role = 'replica';" jq -r '.import.tables_in_order[]' manifest.json | while read -r TABLE; do echo "\\COPY v2.${TABLE} FROM '/tmp/import/data/${TABLE}.csv' WITH CSV HEADER" done -} | kubectl exec -i -n permit-platform $PG_POD -- psql -U permit -d permit --single-transaction + echo 'COMMIT;' +} | kubectl exec -i -n permit-platform $PG_POD -- psql -U permit -d permit ``` -:::info Why referential checks are deferred +:::info Why referential checks are turned off during the import Some Permit tables reference each other in both directions, so no import order can satisfy -per-row referential checks - the import therefore defers them for this session +per-row referential checks - the import therefore turns them off for this session (`session_replication_role`). This is safe: your package was exported as a single consistent -database snapshot, so the data is guaranteed to be internally consistent. Use this technique -only for this import - not for ad-hoc database changes. +database snapshot. The one known exception - references to members who left your organization +before the export - is cleaned up in step 6. Use this technique only for this import - not for +ad-hoc database changes. ::: ### 6. Clear references to departed members @@ -240,7 +243,8 @@ kubectl exec -n permit-platform $PG_POD -- psql -U permit -d permit -c " UPDATE v2.v2_member SET is_superuser = true WHERE email = '';" ``` -Log out and log back in for the change to take effect. +After you restart the services in step 9, log out and log back in for the change to take +effect. :::caution Superuser applies platform-wide A superuser can see and manage **all** organizations on your deployment, not just the imported @@ -253,9 +257,10 @@ one. Grant it only to your installation admin account. # Remove the import files from the pod kubectl exec -n permit-platform $PG_POD -- rm -rf /tmp/import -# Scale the services back up (use the replica counts you noted in step 2) -kubectl scale deployment -n permit-platform \ - permit-backend-v2 celery-general permit-dl-enricher-v2 --replicas=1 +# Scale each service back to the replica count you noted in step 2 +kubectl scale deployment -n permit-platform permit-backend-v2 --replicas= +kubectl scale deployment -n permit-platform celery-general --replicas= +kubectl scale deployment -n permit-platform permit-dl-enricher-v2 --replicas= ``` ## Verify the Import @@ -344,10 +349,11 @@ database is unchanged. Fix the cause shown in the error (usually a missing file path), then run step 5 again from the start. If you are unsure of the state, roll back using the backup from step 3, or contact Permit support. -### The import loop prints nothing +### The import prints no `COPY` lines -Your `manifest.json` predates the `import` section. Check the package `README.md` for the -ready-to-run import script matching your package version, or contact Permit support. +Your package was produced before manifests included the import list +(`.import.tables_in_order`). Check the package `README.md` for the ready-to-run import script +matching your package version, or contact Permit support. ### The imported organization is not visible in the UI