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

# Keep Stripe API Keys Off Laptops

> Store the Stripe API key on the Connector and inject it into Stripe API calls with a Native User

export const G = ({term, anchor, children}) => {
  const href = anchor ? `/docs/glossary/index#${anchor}` : `/docs/glossary/index`;
  return <a href={href} className="glossary-link" style={{
    textDecoration: "underline",
    textDecorationLine: "underline",
    textDecorationColor: "#6b7280",
    textDecorationThickness: "1px",
    textUnderlineOffset: "2px",
    color: "inherit",
    transition: "text-decoration-color 0.2s ease",
    borderBottom: "none"
  }} onMouseEnter={e => e.target.style.textDecorationColor = "#fff"} onMouseLeave={e => e.target.style.textDecorationColor = "#6b7280"}>
  {children || term}
</a>;
};

## Overview

You may want employees to use a subset of a third-party production API, such as Stripe, for a narrow set of tasks. Without Formal, this usually means giving them a production [API key](https://docs.stripe.com/keys) with broader permissions than you want to allow. That key then sits on their laptop, in a `.env` file or shell profile.

With Formal, you can use the <G anchor="formal-endpoint">Formal Endpoint</G> and a <G anchor="connector">Connector</G> to:

* Keep the API key off employee laptops. Only the Connector holds it.
* Let employees make authenticated requests to the Stripe API without a credential.
* Write fine-grained [policies](/docs/guides/policies/examples) against every request between the laptop and Stripe.

This guide uses `curl` on macOS.

### How It Works

1. The Formal Endpoint on macOS intercepts `curl` requests to `api.stripe.com` with a [network rule](/docs/guides/network-rules).
2. The Endpoint forwards them to the Connector with the developer's Formal identity.
3. A <G anchor="native-user">Native User</G> on the Stripe <G anchor="resource">resource</G> injects `Authorization: Bearer <STRIPE_API_KEY>`.
4. Stripe receives the real key. The laptop never does.

Clients keep calling `https://api.stripe.com`. You do not change base URLs.

```mermaid theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
sequenceDiagram
  participant App as curl
  participant Endpoint as Formal Endpoint
  participant Connector as Connector
  participant Stripe as api.stripe.com

  App->>Endpoint: GET /v1/balance (no key)
  Endpoint->>Connector: Forward with Formal identity
  Connector->>Connector: Inject Native User key
  Connector->>Stripe: Authorization: Bearer <STRIPE_API_KEY>
  Stripe-->>App: Response
```

## Prerequisites

* <G anchor="formal-endpoint">Formal Endpoint</G> on macOS with [Transparent Mode](/docs/guides/client-apps/desktop-app#transparent-mode) enabled
* A deployed <G anchor="connector">Connector</G> the Endpoint can reach
* Stripe API Key

## 1. Store the key on the Connector

Set the key as the `STRIPE_API_KEY` environment variable on the Connector.

## 2. Create the Stripe resource

<Tabs>
  <Tab title="Control Plane">
    1. Navigate to [Resources](https://app.formal.ai/resources) and click **Create Resource**
    2. Set **Technology** to **HTTP**
    3. Set **Name** to `stripe`
    4. Set **Hostname** to `api.stripe.com` and **Port** to `443`
    5. Save the resource
    6. Add a TLS configuration to the resource and select **Verify Full**
  </Tab>

  <Tab title="Terraform">
    ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    resource "formal_resource" "stripe" {
      name       = "stripe"
      technology = "http"
      hostname   = "api.stripe.com"
      port       = 443
    }

    resource "formal_resource_tls_configuration" "stripe" {
      resource_id = formal_resource.stripe.id
      tls_config  = "verify-full"
    }
    ```
  </Tab>
</Tabs>

[Verify Full](/docs/guides/core-concepts/resources/tls#tls-between-the-connector-and-resources) makes the Connector check the certificate chain and hostname of `api.stripe.com` before it sends the API key.

## 3. Add a Native User for the key

The Native User tells the Connector to set `Authorization: Bearer <key>` on every Stripe request. It replaces any `Authorization` header the client sent.

<Tabs>
  <Tab title="Control Plane">
    1. Open the `stripe` resource and select **Authentication**
    2. Click **Add User** and select **HTTP Bearer**
    3. Set **Label** to `stripe-api-key`
    4. Choose **Static**, set the header to `Authorization`, and choose **Environment Variable**
    5. Enter `STRIPE_API_KEY` and click **Create**
    6. Copy the Native User ID from its action menu
    7. Under **Default Native User**, paste that ID as the CEL expression and click **Save**

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

  <Tab title="Terraform">
    ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    resource "formal_native_user_v3" "stripe" {
      resource_id = formal_resource.stripe.id
      label       = "stripe-api-key"

      http_bearer {
        header = "Authorization"
        token {
          environment_variable = "STRIPE_API_KEY"
        }
      }
    }

    resource "formal_resource_native_user_selection" "stripe" {
      resource_id = formal_resource.stripe.id
      cel         = "\"${formal_native_user_v3.stripe.id}\""
    }
    ```
  </Tab>
</Tabs>

<Warning>
  Set a default Native User. Without one, HTTP resources forward requests
  without an injected key, and Stripe returns `401`.
</Warning>

See [Native Users](/docs/guides/core-concepts/resources/native-users#select-a-default-native-user) for all selection fields.

**Verify:** The `stripe-api-key` Native User appears under **Native Users** with type **HTTP Bearer** and source `STRIPE_API_KEY`.

## 4. Make Stripe reachable through the Connector

<Tabs>
  <Tab title="Control Plane">
    1. Open your Connector and select **Listeners**
    2. Create a listener on an available port, such as `443`. Port `8080` is reserved for health checks.
    3. Open the listener, click **New Rule**, set the type to **Resource**, and select `stripe`
  </Tab>

  <Tab title="Terraform">
    ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    resource "formal_connector_listener" "stripe" {
      connector_id = formal_connector.example.id
      name         = "stripe-listener"
      port         = 443
    }

    resource "formal_connector_listener_rule" "stripe" {
      connector_listener_id = formal_connector_listener.stripe.id
      type                  = "resource"
      rule                  = formal_resource.stripe.id
    }
    ```
  </Tab>
</Tabs>

**Verify:** `stripe` appears under the Connector's **Reachable Resources**.

## 5. Forward Stripe traffic to the Connector

Create a network rule that intercepts `curl` requests to `api.stripe.com` and forwards them to the Connector.

Match `curl` by its code signing identifier in `pre_tcp`, and check the hostname in `pre_tls`. Find the identifier with `codesign`:

```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
codesign -dvv $(which curl) 2>&1 | grep '^Identifier'
# Identifier=com.apple.curl
```

<Tabs>
  <Tab title="Control Plane">
    1. Navigate to [Network Rules](https://app.formal.ai/network-rules)
    2. Create a rule named `stripe-curl`
    3. Paste the CEL below
    4. Save the rule and set it to **Active**

    ```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    {
      "condition": {
        "pre_tcp": process.signing.code_identifier == "com.apple.curl",
        "pre_tls": hostname == "api.stripe.com",
        "post_tls": true
      },
      "outputs": {
        "resource_name": "stripe",
        "forward_to_connector": true
      }
    }
    ```
  </Tab>

  <Tab title="Terraform">
    ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    resource "formal_network_rule" "stripe_curl" {
      name        = "stripe-curl"
      description = "Forward curl requests to Stripe to the Connector for key injection"
      status      = "active"

      cel_expression = <<-EOT
        {
          "condition": {
            "pre_tcp": process.signing.code_identifier == "com.apple.curl",
            "pre_tls": hostname == "api.stripe.com",
            "post_tls": true
          },
          "outputs": {
            "resource_name": "stripe",
            "forward_to_connector": true
          }
        }
      EOT
    }
    ```
  </Tab>
</Tabs>

<Warning>
  **Forward to Connector** must be `true`. Without it, the Endpoint evaluates
  the request locally, and no Native User injects the key.
</Warning>

## 6. Restrict Stripe requests with a policy

The default Native User injects the key into every request that reaches the `stripe` resource. Add a policy that blocks every Stripe request except the ones your developers need.

This example lets the `payments-engineers` group read balances, charges, and customers. It blocks all other Stripe requests, including writes and requests from other users.

<Tabs>
  <Tab title="Control Plane">
    1. Navigate to [Policies](https://app.formal.ai/policies)
    2. Click **Create Policy**
    3. Set name to `stripe-read-only` and add a description
    4. Paste the Rego below into the editor
    5. Click **Create Policy** to save

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

    import future.keywords.if
    import future.keywords.in

    allowed_paths := {"/v1/balance", "/v1/charges", "/v1/customers"}

    allowed if {
      "payments-engineers" in input.user.groups
      input.http.method == "GET"
      some path in allowed_paths
      startswith(concat("", [input.http.path, "/"]), concat("", [path, "/"]))
    }

    request := {
      "action": "block",
      "type": "block_with_formal_message",
      "reason": "This Stripe request is not allowed"
    } if {
      input.resource.hostname == "api.stripe.com"
      not allowed
    }
    ```
  </Tab>

  <Tab title="Terraform">
    ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    resource "formal_policy" "stripe_read_only" {
      name        = "stripe-read-only"
      description = "Allow payments engineers to read Stripe balances, charges, and customers"
      status      = "active"

      module = <<-EOT
        package formal.v2

        import future.keywords.if
        import future.keywords.in

        allowed_paths := {"/v1/balance", "/v1/charges", "/v1/customers"}

        allowed if {
          "payments-engineers" in input.user.groups
          input.http.method == "GET"
          some path in allowed_paths
          startswith(concat("", [input.http.path, "/"]), concat("", [path, "/"]))
        }

        request := {
          "action": "block",
          "type": "block_with_formal_message",
          "reason": "This Stripe request is not allowed"
        } if {
          input.resource.hostname == "api.stripe.com"
          not allowed
        }
      EOT
    }
    ```
  </Tab>
</Tabs>

Adjust `allowed_paths`, the methods, and the group to match what your developers need. Find Stripe paths in the [Stripe API reference](https://docs.stripe.com/api). See [HTTP object](/docs/guides/policies/evaluation#http-object) for all request fields.

**Verify:** A `GET /v1/balance` from a `payments-engineers` member succeeds. A `POST /v1/refunds` returns the Formal block message.

## 7. Call Stripe without a key

Remove Stripe keys from `.env` files, shell profiles, and CLI configs on the laptop. Then call Stripe with `curl` and no key:

```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
curl https://api.stripe.com/v1/balance
```

**Verify:**

1. Open [Logs](https://app.formal.ai/logs) and filter on the `stripe` resource
2. Confirm the request shows the developer's Formal identity
3. Confirm the response status is `200`

## Troubleshooting

<AccordionGroup>
  <Accordion title="Requests do not appear in Formal logs">
    **Possible causes:**

    * Transparent Mode is off
    * The network rule is in **Draft**
    * `curl` is not `/usr/bin/curl`, for example Homebrew `curl`, so its code signing identifier differs

    **Fix:**

    1. Run `formal transparent-proxy status`
    2. Set the `stripe-curl` network rule to **Active**
    3. Run `codesign -dvv $(which curl)` and use its `Identifier` in `pre_tcp`
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Stripe API Keys" icon="stripe" href="https://docs.stripe.com/keys">
    Manage Stripe API keys
  </Card>

  <Card title="Native Users" icon="user" href="/docs/guides/core-concepts/resources/native-users">
    Select credentials by identity or with hooks
  </Card>

  <Card title="HTTP Resources" icon="globe" href="/docs/guides/core-concepts/resources/http">
    Write policies for HTTP traffic
  </Card>

  <Card title="Network Rules" icon="filter" href="/docs/guides/network-rules">
    Choose which traffic the Endpoint intercepts
  </Card>

  <Card title="Encrypt MCP OAuth Tokens" icon="key" href="/docs/guides/example-use-cases/encrypt-mcp-oauth-tokens">
    Keep OAuth tokens encrypted on the device
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.