---
title: Run Apps Without Static Secrets | Keycard
description: Deploy services that authenticate themselves without API keys or secrets in environment variables.
---

Ask Claude to deploy your application to Fly.io. The deployed service identifies itself with a runtime OIDC token — no API keys to store or rotate.

Register ProviderTrust Fly.io’s OIDC issuer in Keycard

Deploy AppApp runs on Fly.io runtime

Token ExchangeRuntime OIDC token → Keycard credential

No SecretsEphemeral credentials, nothing to rotate

## Prerequisites

- **Application running locally** from [Access APIs on Behalf of Users](/guides/access-apis-on-behalf-of-users/index.md)

- **Fly.io account** with `flyctl` installed ([sign up](https://fly.io) if you don’t have one)

  Tip

  You can ask Claude to install `flyctl` for you if you don’t have it yet.

- Your `keycard run -- claude` session should still be running

## Walkthrough

1. **Ask Claude to deploy**

   Inside your Keycard session, send a prompt like:

   > Deploy my MCP server to Fly.io.

2. **Authenticate to Fly.io**

   There are two ways to give Claude access to Fly.io for deployment. Pick the one that fits your workflow:

   - [Keycard-brokered access](#tab-panel-66)
   - [Direct login](#tab-panel-67)

   Fly.io doesn’t support federated token exchange, so you store a Fly.io deploy token in Keycard as a [vaulted static credential](/concepts/resources/#vaulted-static-credentials/index.md). Keycard brokers access without persisting credentials on your machine.

   Claude handles the setup: it creates the vault resource, checks your access policy, and links you to the Keycard Console credentials page. Create a deploy token at [fly.io/tokens](https://fly.io/tokens) scoped to your Fly.io organization, then open the link Claude gives you and paste the token under **Credentials**.

   The token is encrypted at rest. Claude never sees the raw value; it only receives it through token exchange at deployment time. When Claude needs the token, Keycard shows a consent screen so you can approve before it proceeds:

   ![Keycard consent screen for Fly.io](/_astro/flyio-consent-screen.CbVe0AE9_Z1qumvR.webp)

   Caution

   Never paste credentials into an agent conversation. Always use [Keycard Console](https://console.keycard.ai) to store credentials. This keeps secrets off the LLM path entirely.

   Note

   After storing the token, you’ll need to restart your `keycard run` session so it picks up the new credential. Claude will tell you when this is necessary and how to resume.

   Run `flyctl auth login` in your terminal. This authenticates your local user to Fly.io, and Claude can use `flyctl` commands on your behalf within the Keycard session.

   Terminal window

   ```
   flyctl auth login
   ```

   Use a Keycard [access policy](/admin/access-policies/index.md) to restrict which `flyctl` commands the agent can run. For example, you can allow `flyctl deploy` but require ITL approval for `flyctl destroy`.

   Caution

   This is the quickest path. The downside is that `flyctl` credentials persist on your machine beyond the agent session.

3. **Claude configures and deploys**

   Claude generates the Fly.io config, wires up Keycard trust for workload identity, registers the production resource, and deploys the container.

   Tip

   While Claude is building out the deployment, this is a good time to read the [Patterns](#patterns) section below.

   Here’s how the scaffolded code handles credential discovery and workload identity:

   - [Credential Discovery](#tab-panel-64)
   - [Workload Identity](#tab-panel-65)

   ```
   // credentials.ts - auto-detects the runtime and picks the right credential
   export function discoverApplicationCredential(
     options: DiscoverOptions,
   ): ApplicationCredential {
     const clientId = process.env.KEYCARD_CLIENT_ID;
     const clientSecret = process.env.KEYCARD_CLIENT_SECRET;
     if (clientId && clientSecret) {
       return new ClientSecret(clientId, clientSecret); // local dev via keycard run
     }


     if (process.env.FLY_APP_NAME) {
       return new FlyWorkloadIdentity({ audience: options.zoneUrl }); // production
     }
     // ... EKS, web identity, and other runtimes
   }
   ```

   ```
   // credentials.ts - Fly.io OIDC token fetch via the runtime metadata endpoint
   class FlyWorkloadIdentity implements ApplicationCredential {
     async prepareTokenExchangeRequest(subjectToken: string, resource: string) {
       const oidcToken = await this.#getToken(); // fetched from /.fly/api
       return {
         subjectToken,
         resource,
         clientAssertionType: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
         clientAssertion: oidcToken,
       };
     }
   }
   ```

4. **Test the production deployment**

   Your MCP server is live at `https://<project-name>.fly.dev`. Ask Claude to call one of your server’s tools. The same tools that worked locally now run through the deployed server, but the server authenticates with workload identity instead of `keycard run`.

5. **Verify in Keycard Console**

   Open [Keycard Console](https://console.keycard.ai) > **Audit Log**. You should see entries for the tool call flowing through your production server:

   |                      |                            |
   | -------------------- | -------------------------- |
   | `users:authenticate` | You logged in successfully |
   | `users:authorize`    | Your access was authorized |
   | `credentials:issue`  | Access token was issued    |

   Same event types as local development. Authorization and audit don’t change based on where the server runs.

## Patterns

Your service is live with no stored secrets. Here’s what Claude set up and how to apply it to other runtimes.

### Workload Identity

Locally, `keycard run` injects credentials at startup. In production there’s no human present, so the deployed service needs its own identity to authenticate with Keycard and request resources on behalf of callers.

Claude registered a Fly.io OIDC provider in your Keycard instance. When your server starts on Fly.io, it gets an OIDC token from the runtime. Keycard verifies that token against the registered provider and issues credentials — no stored secrets involved.

Fly.io Runtime

MCP Server

Resource

Keycard

ephemeral credential

Fly.io Runtime

OIDC token

MCP Server

tokenexchangeKeycard

ephemeral credential

Resource

The application credential Claude configured is what ties this together. It specifies which Fly.io workloads can act as your application, scoped to your organization and app name. See [Applications](/platform/concepts/applications/index.md) for the identity and credential model.

### Vaulted Static Credentials

Some upstream resources only offer static API keys or long-lived tokens — no ephemeral token exchange. For those, Keycard stores the credential as a [vaulted static credential](/platform/concepts/resources/#vaulted-static-credentials/index.md) and brokers access through token exchange.

Only use vaulted credentials when native token exchange isn’t available. They have two drawbacks:

- You need to manually rotate the credential in [Keycard Console](https://console.keycard.ai) when it expires or gets compromised.
- Every application that requests this resource receives the same static credential, so you can’t scope access per-caller or per-request.

If a resource supports OIDC or OAuth token exchange, use that instead. Ephemeral tokens expire on their own and don’t need rotation.

### Production Resource Configuration

Locally, your MCP server’s resource identifier points to `localhost`. In production, Keycard needs the server’s public URL so callers can find it.

Claude registered a second resource (`https://<project-name>.fly.dev/mcp`) alongside the existing `localhost` resource. Both belong to the same application. MCP clients use this identifier to locate your server, and callers authenticate through Keycard, which sends them to the right endpoint.

Both resources are visible in [Keycard Console](https://console.keycard.ai) under your application’s resource configuration.

## Troubleshooting

Claude is still calling `localhost` instead of the deployed URL

After deployment, Claude’s MCP client config may still point to `http://localhost:3000/mcp`. Ask Claude to update the MCP config to use the new production address (`https://<project-name>.fly.dev/mcp`), then restart the MCP connection.

Token exchange fails with “provider not found”

Keycard doesn’t recognize the Fly.io OIDC issuer yet. Ask Claude to register the Fly.io OIDC provider in your Keycard zone, or check [Keycard Console](https://console.keycard.ai) > **Providers** to verify it’s listed.

Deployment succeeds but the server returns 401

The application credential may not match the deployed workload. Verify in [Keycard Console](https://console.keycard.ai) > **Applications** that the credential’s Fly.io app name and organization match your deployment. A mismatch means the runtime OIDC token won’t pass validation.

## What’s next

Your service is running in production with no managed credentials. The next guide covers giving an agent its own access to third-party APIs, with no human in the loop.

- **[Grant Agent Access to APIs](/guides/grant-agent-access-to-apis/index.md)**: build an agent that authenticates to external services like Snowflake
