> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Native Users

> Configure the credentials that Connectors use for upstream resources

## Overview

Native Users define how a Connector authenticates to an upstream Resource.
Formal identities never need to know or send these upstream credentials.

<Note>
  This guide documents Native Users supported by Connector 2.17.0 and later.
  If your Resource still uses the legacy model, see
  [Legacy Native Users](/docs/guides/core-concepts/resources/native-users-legacy).

  If you would like to migrate to new Native Users, see the [migration guide](/docs/guides/core-concepts/resources/native-users-legacy#migrate-to-new-native-users).

  gRPC Resources do not support either model.
</Note>

## How Native Users work

Each Native User has three important properties:

* A **label** identifies the Native User within its Resource.
* A **credential type** defines how the Connector authenticates upstream.
* A **credential source** supplies static values or runs a hook at connection time.

You can configure a default Native User selection for each Formal identity.
The selection returns a Native User ID.

A user can instead request a Native User by appending `@<label>` to their Formal
username. The requested Native User takes precedence over the default selection.

## Supported credential types

The Formal console shows only the credential types supported by the Resource:

| Credential type  | `user_type`           | Connector behavior                                          |
| ---------------- | --------------------- | ----------------------------------------------------------- |
| Password         | `basic`               | Sends an upstream username and password                     |
| SSH Key          | `ssh_key`             | Uses an SSH username, private key, and optional certificate |
| Snowflake Key    | `snowflake_key`       | Uses a Snowflake username and private key                   |
| AWS IAM          | `aws_iam`             | Uses the Connector's ambient AWS credentials                |
| AWS IAM Role     | `aws_iam_role`        | Assumes a specified AWS role                                |
| GCP IAM          | `gcp_iam`             | Uses the Connector's ambient GCP service account            |
| Azure IAM        | `azure_iam`           | Uses the Connector's ambient Microsoft Entra identity       |
| Kubeconfig Path  | `kubernetes_path`     | Reads a kubeconfig file on the Connector                    |
| Kubeconfig       | `kubernetes_inline`   | Uses an inline kubeconfig document                          |
| HTTP Basic       | `http_basic`          | Injects Basic credentials into a named header               |
| HTTP Bearer      | `http_bearer`         | Injects a bearer token into a named header                  |
| API Key (Header) | `http_api_key_header` | Injects an API key into a named header                      |
| API Key (Query)  | `http_api_key_query`  | Injects an API key into a query parameter                   |

<Tabs>
  <Tab title="Web">
    ## Create a Native User

    1. Go to **Resources** and open your Resource.
           <img src="https://mintcdn.com/formal/z9GNtynV1YHUDYCN/assets/images/native_user1.png?fit=max&auto=format&n=z9GNtynV1YHUDYCN&q=85&s=df525a533918f8c12be630e4708b0151" alt="Resources list with a Resource ready to open" width="1512" height="982" data-path="assets/images/native_user1.png" />
    2. Select **Authentication**.
    3. Click **Add User**.
           <img src="https://mintcdn.com/formal/z9GNtynV1YHUDYCN/assets/images/native_user2.png?fit=max&auto=format&n=z9GNtynV1YHUDYCN&q=85&s=a730fbbf5bb9db59396e5c871d6e0c1c" alt="Authentication tab with the Native Users section and Add User button" width="1836" height="1886" data-path="assets/images/native_user2.png" />
    4. Select a credential type.
           <img src="https://mintcdn.com/formal/z9GNtynV1YHUDYCN/assets/images/native_user3.png?fit=max&auto=format&n=z9GNtynV1YHUDYCN&q=85&s=2b4dab8fdc133b114381f0bdc71096ae" alt="Add Native User credential type picker" width="1152" height="732" data-path="assets/images/native_user3.png" />
    5. Enter a unique **Label**.
    6. Choose **Static** or **Hook** as the credential source.
    7. Configure the credential fields and termination protection.
    8. Click **Create**.

    For static secrets, choose **Value** to store an encrypted value in Formal.
    Choose **Environment Variable** to resolve it on the Connector.

    Enter only the environment variable name, such as
    `ANALYTICS_READONLY_PASSWORD`. Do not use the legacy `ENV:` prefix.

    **Verify:** Confirm that the Native User appears under **Native Users** with
    the expected label, credential type, and authentication source.

    ### Resolve credentials with a hook

    Choose **Hook** when credentials must be generated or fetched at connection
    time. The hook runs on the Connector after policy allows the connection.

    Select the credential type before writing the hook. The hook must return that
    type's exact credential shape.

    Allowlist every environment variable the hook reads. Access each value through
    the hook's `env` argument.

    The `input` argument contains the Resource and selected Native User:

    ```ts theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    type HookNativeUserInput = {
      resource: {
        id: string;
        name: string;
        technology: string;
        hostname: string;
        port: number;
        environment: string;
        provider: string;
      };
      native_user: {
        id: string;
        label: string;
        output_type: string;
      };
    };
    ```

    The Formal identity is intentionally absent from hook input. Use the
    [default selection](#select-a-default-native-user) to choose credentials by
    identity.

    #### Hook examples

    The editor provides `HookNativeUserInput`, `HookNativeUserEnvironment`, and
    `HookNativeUserOutput` types for your selected credential type.

    <AccordionGroup>
      <Accordion title="Read a database password from the Connector">
        Select **Password** and allowlist `DATABASE_PASSWORD`.

        ```ts theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
        export default async function credentials(
          input: HookNativeUserInput,
          env: HookNativeUserEnvironment,
        ): Promise<HookNativeUserOutput> {
          const password = env["DATABASE_PASSWORD"];
          if (!password) {
            throw new Error("DATABASE_PASSWORD is not set");
          }

          return {
            username: input.native_user.label,
            password,
          };
        }
        ```
      </Accordion>

      <Accordion title="Fetch database credentials from Vault">
        Select **Password**. Allowlist the `vault.example.com` network host and
        the `VAULT_TOKEN` environment variable.

        ```ts theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
        export default async function credentials(
          _input: HookNativeUserInput,
          env: HookNativeUserEnvironment,
        ): Promise<HookNativeUserOutput> {
          const token = env["VAULT_TOKEN"];
          if (!token) {
            throw new Error("VAULT_TOKEN is not set");
          }

          const response = await fetch(
            "https://vault.example.com/v1/database/creds/readonly",
            {
              headers: { "x-vault-token": token },
            },
          );
          if (!response.ok) {
            throw new Error(`Vault returned ${response.status}`);
          }

          const result = (await response.json()) as {
            data?: { username?: string; password?: string };
          };
          if (!result.data?.username || !result.data.password) {
            throw new Error("Vault returned invalid credentials");
          }

          return {
            username: result.data.username,
            password: result.data.password,
          };
        }
        ```
      </Accordion>

      <Accordion title="Mint an HTTP bearer token">
        Select **HTTP Bearer**. Allowlist the `auth.example.com` network host.
        Allowlist the `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` environment
        variables.

        ```ts theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
        export default async function credentials(
          _input: HookNativeUserInput,
          env: HookNativeUserEnvironment,
        ): Promise<HookNativeUserOutput> {
          const clientId = env["OAUTH_CLIENT_ID"];
          const clientSecret = env["OAUTH_CLIENT_SECRET"];
          if (!clientId || !clientSecret) {
            throw new Error("OAuth client credentials are not set");
          }

          const body = [
            "grant_type=client_credentials",
            `client_id=${encodeURIComponent(clientId)}`,
            `client_secret=${encodeURIComponent(clientSecret)}`,
          ].join("&");
          const response = await fetch("https://auth.example.com/oauth/token", {
            method: "POST",
            headers: { "content-type": "application/x-www-form-urlencoded" },
            body,
          });
          if (!response.ok) {
            throw new Error(`OAuth server returned ${response.status}`);
          }

          const result = (await response.json()) as { access_token?: string };
          if (!result.access_token) {
            throw new Error("OAuth server did not return an access token");
          }

          return {
            header: "Authorization",
            token: result.access_token,
          };
        }
        ```
      </Accordion>
    </AccordionGroup>

    Network access is denied unless you allowlist the destination hostname.
    See [Hooks](/docs/guides/policies/hooks) for network and runtime limits.

    ### Rotate or edit credentials

    Select a Native User to open its details. You can rename its label or change
    termination protection.

    Use **Update Credentials** to rotate static credentials. You cannot change a
    Native User's credential type or switch between static and hook sources.
    Create another Native User for those changes.

    <img src="https://mintcdn.com/formal/z9GNtynV1YHUDYCN/assets/images/native_user4.png?fit=max&auto=format&n=z9GNtynV1YHUDYCN&q=85&s=1d73b872b65cab4d18ce20fcca43f9b1" alt="Native User details with credential and termination protection settings" width="1152" height="1024" data-path="assets/images/native_user4.png" />

    ## Select a default Native User

    **Default Native User** maps a Formal identity to a Native User ID.
    You can copy a Native User's ID from its action menu to reference it in the CEL expression.

    <img src="https://mintcdn.com/formal/z9GNtynV1YHUDYCN/assets/images/native_user5.png?fit=max&auto=format&n=z9GNtynV1YHUDYCN&q=85&s=ebe7ccc7a01205cf56d48bc684625007" alt="Default Native User CEL expression editor" width="947" height="287" data-path="assets/images/native_user5.png" />

    Return one ID to use the same Native User for everyone:

    ```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    "nativeuser_01km5mz0x5t6t9gs195s5dhe73" // readonly
    ```

    Use a conditional expression to select by identity:

    ```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    "admins" in user.groups
      ? "nativeuser_01k3wxrm9zyfj2yce5m2w0xcav" // admin
      : "nativeuser_01km5mz0x5t6t9gs195s5dhe73" // readonly
    ```

    The `user` object supports these fields:

    | Field               | Type            |
    | ------------------- | --------------- |
    | `user.id`           | String          |
    | `user.email`        | String          |
    | `user.username`     | String          |
    | `user.type`         | String          |
    | `user.external_ids` | List of strings |
    | `user.groups`       | List of strings |
    | `user.group_ids`    | List of strings |

    CEL result branches must contain literal Native User IDs. Computed result
    strings are rejected.

    Click **Save** after the editor shows **Valid CEL**.

    **Verify:** Connect without an `@<label>` suffix and confirm the expected
    upstream credential identity appears in the session log.

    ### Require an explicit selection

    Leave **Default Native User** unset to require clients to request a label.
    HTTP Resources instead proceed without injected credentials.

    A CEL branch can return an empty string to reject matching identities:

    ```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    user.type == "machine"
      ? "nativeuser_01kw0tsje6ztr0sw1fyr4t3845" // service account
      : ""
    ```

    This rejection also applies when the client requests a label explicitly.
  </Tab>

  <Tab title="Terraform">
    ## Create a Native User

    Define each Native User with a typed credential block:

    ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    resource "formal_resource" "database" {
      name       = "analytics-postgres"
      hostname   = "analytics.internal"
      technology = "postgres"
      port       = 5432
    }

    resource "formal_native_user_v3" "admin" {
      resource_id = formal_resource.database.id
      label       = "admin"

      basic {
        username = "app_admin"
        password {
          environment_variable = "ANALYTICS_ADMIN_PASSWORD"
        }
      }
    }

    resource "formal_native_user_v3" "read_only" {
      resource_id = formal_resource.database.id
      label       = "read-only"

      basic {
        username = "app_readonly"
        password {
          environment_variable = "ANALYTICS_READONLY_PASSWORD"
        }
      }
    }

    resource "formal_resource_native_user_selection" "database" {
      resource_id = formal_resource.database.id

      cel = <<-CEL
        "admins" in user.groups
          ? "${formal_native_user_v3.admin.id}"
          : "${formal_native_user_v3.read_only.id}"
      CEL
    }
    ```

    An `environment_variable` value names an environment variable on the
    Connector. A `literal` value stores the secret in Terraform state.

    **Verify:**

    ```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    terraform plan
    terraform apply
    ```

    Confirm that the plan creates two `formal_native_user_v3` resources and one
    `formal_resource_native_user_selection`.

    ### Use a hook

    Hooks run on the Connector at connection time. Declare the output shape and
    allowlist each environment variable:

    ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    resource "formal_native_user_v3" "dynamic" {
      resource_id = formal_resource.database.id
      label       = "dynamic"

      hook {
        output_type               = "basic"
        allowlisted_env_variables = ["FORMAL_ENV_USER", "FORMAL_ENV_PASSWORD"]
        code                      = <<-TYPESCRIPT
          export default async function (_input, env) {
            return {
              username: env.FORMAL_ENV_USER,
              password: env.FORMAL_ENV_PASSWORD,
            };
          }
        TYPESCRIPT
      }
    }
    ```
  </Tab>
</Tabs>

## Request a Native User at connection time

Append `@<label>` to the Formal username:

```text theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
<formal_username>@<native_user_label>
```

For example:

```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
psql \
  --host "<CONNECTOR_HOSTNAME>" \
  --port 5432 \
  --dbname "<DATABASE_NAME>" \
  --username "idp:formal:human:john@example.com@read-only"
```

The label selects the Native User. It does not need to match the upstream
username.

An explicit request takes precedence over the assigned default. Use policies to
restrict overrides and privileged labels.

## Control explicit overrides with policies

Native Users expose both the requested Native User and assigned default to
policies. Each object contains `id`, `label`, and `user_type`.

This session policy blocks an explicit request that differs from the assigned
default:

```rego theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
package formal.v2

import future.keywords.if

default session := {"action": "allow"}

session := {
  "action": "block",
  "type": "block_with_formal_message",
  "message": "This Native User is not assigned to your identity"
} if {
  input.native_user_assignment == "user-requested"
  input.native_user_selection.requested.id != input.native_user_selection.assigned.id
}
```

See [policy evaluation](/docs/guides/policies/evaluation#native-users) for the
complete input shape.

## Understand Native Users in logs

For Formal-authenticated sessions, `user.formal.native` contains the selected
Native User's upstream username when applicable.

When a client connects directly with Resource credentials, `user.type` is
`native`. Formal identity fields and `user.formal.native` are absent.
