# Tundri & Permifrost — Snowflake Access Control

Skills for managing Snowflake access control using [tundri](https://github.com/Gemma-Analytics/tundri) and [Permifrost](https://github.com/Gemma-Analytics/permifrost).

Tundri runs `DROP`, `CREATE`, and `ALTER` statements against Snowflake and uses Permifrost to handle `GRANT`/`REVOKE` commands for permissions management. Configuration is declarative via a `permifrost.yml` file committed to a Git repository. CI/CD runs tundri automatically on every merge, keeping Snowflake state in sync with the repo.

The `permifrost` service user is the only user you create manually. All other users — service accounts (Airflow, dbt, Airbyte, Metabase) and human analysts — are declared in `permifrost.yml` and created by tundri on its first run.

## When to use this

- **New client onboarding:** follow the setup sequence below from step 1 to step 5 in order.
- **Ongoing operations:** add or remove users ([skill 6](#6-add-remove-permifrost-user)), rotate keys ([snowflake-rotate-keypair](../skills/snowflake-rotate-keypair/)), test authentication ([snowflake-test-keypair-auth](../skills/snowflake-test-keypair-auth/)).

## Related skills

| Skill | Description |
|---|---|
| [1password](../../../../security/1password/) | Store and retrieve secrets using the 1Password CLI (`op`) — used throughout for managing Snowflake credentials |

---

## Initial client setup

Follow these skills in order when setting up a new client. For ongoing operations, use the individual skills as needed.

| # | Skill | Description |
|---|---|---|
| 1 | [setup-tundri-repository](../skills/setup-tundri-repository/) | Scaffold a new `<client>-snowflake` GitHub repository with `permifrost.yml`, CI/CD workflows, and client README |
| 2 | [setup-tundri-snowflake-account](../skills/setup-tundri-snowflake-account/) | Create the Snowflake trial account, configure the `admin` user with MFA, and bootstrap the `permifrost` service user |
| 3 | [snowflake-generate-keypair](../skills/snowflake-generate-keypair/) | Generate RSA key pairs for all remaining service users (airflow, dbt, airbyte, metabase) — keys stored in 1Password, never on disk |
| 4 | [setup-tundri-local-dev](../skills/setup-tundri-local-dev/) | Configure the local Python environment, populate `.env` from 1Password via `op inject`, run a dry run |
| 5 | [setup-tundri-cicd](../skills/setup-tundri-cicd/) | Wire up GitHub Actions (or GitLab CI) for automated dry runs on PRs and production runs on merge |
| 6 | [add-remove-permifrost-user](../skills/add-remove-permifrost-user/) | Add or remove a human or service user in `permifrost.yml` (the standard ongoing operation) |

---

## Ongoing operations

### Add or remove a user

Edit `permifrost.yml` following the [add-remove-permifrost-user](../skills/add-remove-permifrost-user/) skill. Open a PR — CI runs a dry run and shows the planned SQL. Merge to apply.

### Rotate a service user's key pair

Use [snowflake-rotate-keypair](../skills/snowflake-rotate-keypair/) — zero-downtime rotation using Snowflake's dual key slots (`RSA_PUBLIC_KEY` / `RSA_PUBLIC_KEY_2`).

### Test or debug key-pair authentication

Use [snowflake-test-keypair-auth](../skills/snowflake-test-keypair-auth/) to verify a user's public key fingerprint matches the registered key in Snowflake.

---

## Common errors

### `Warehouse 'COMPUTE_WH' does not exist or not authorized`

Snowflake creates a default `COMPUTE_WH` warehouse on new accounts. If the DROP step in [setup-tundri-snowflake-account](../skills/setup-tundri-snowflake-account/) was skipped, go back and run:

```sql
USE ROLE accountadmin;
DROP WAREHOUSE IF EXISTS compute_wh;
```

### `JWT token is invalid`

Mismatch between the private key being used and the public key registered in Snowflake. Verify:
1. `PERMISSION_BOT_KEY_PATH` in `.env` points to the correct `.p8` file
2. `PERMISSION_BOT_KEY_PASSPHRASE` matches the passphrase used at key generation
3. The public key in `permifrost.yml` (`rsa_public_key`) matches the generated `.pub` file

If in doubt, re-register the public key:

```sql
USE ROLE securityadmin;
ALTER USER permifrost SET rsa_public_key = '<correct_public_key>';
```

### `Password empty`

The `.env` file is not in the same directory as `permifrost.yml`. Tundri auto-loads `.env` only from the same directory. Check paths and retry.

---

## Further reading

- [tundri repository](https://github.com/Gemma-Analytics/tundri) — full CLI reference and changelog
- [Permifrost repository](https://github.com/Gemma-Analytics/permifrost) — YAML spec reference
- [Gemma Confluence: Permifrost Setup](https://gemmaanalytics.atlassian.net/wiki/spaces/TEC/pages/1745518602/Snowflake+-+Permifrost+Setup) — internal context and history
- [Gemma Confluence: Snowflake Setup Checklist](https://gemmaanalytics.atlassian.net/wiki/spaces/TEC/pages/2210234369/Snowflake+Setup+Checklist) — checklist for new client onboarding
- [Snowflake access control overview](https://docs.snowflake.com/en/user-guide/security-access-control-overview)
- [Snowflake system-defined roles](https://docs.snowflake.com/en/user-guide/security-access-control-overview#system-defined-roles)
