---
title: Deploy to Render without Secrets | Keycard
description: Deploy an MCP server on Render using managed OIDC workload identity instead of a client secret
---

You built an MCP server that calls external APIs on behalf of users. Now you want to run it in production without copying a `KEYCARD_CLIENT_SECRET` into your hosting platform: a long-lived secret that can leak, needs rotation, and grants whoever holds it your Application’s full identity.

Render’s [managed OIDC](https://render.com/docs/oidc) removes that secret entirely. Render mints a short-lived, automatically rotated identity token for each service, and Keycard accepts that token as your Application’s credential through **workload identity federation**. Your service proves who it is with its runtime identity. There is no secret to provision, store, or rotate.

Tip

Complete the [Call External APIs from MCP](/guides/delegated-access/index.md) guide first. This guide deploys the server you built there.

## How it works

```
flowchart LR
    A[Render] -->|"Mounts rotating OIDC token"| B[Your MCP Server]
    B -->|"Token exchange + client assertion"| C[Keycard]
    C -->|"Verify issuer + subject"| C
    C -->|"Delegated API token"| B
    B -->|"Bearer token"| D[External API]
```

1. Render issues a short-lived OIDC token for your service and rotates it automatically. The token’s issuer is `https://oidc.render.com/<render-workspace-id>` and its subject identifies your exact service: `workspace:<render-workspace-id>:environment:<environment-id>:service:<service-id>` (the environment segment is `default` when the service is not in a project environment)
2. The Keycard SDK reads the token on every request and presents it as an authentication credential to Keycard
3. Keycard verifies the token’s signature against Render’s published keys, then matches the issuer and subject against the workload identity credential registered on your Application
4. Token exchange proceeds exactly as before: your tools receive delegated, per-user API tokens

Because Keycard pins the credential to your service’s exact subject, no other service, even in the same Render workspace, can authenticate as your Application.

## Prerequisites

- **Render workspace** on a *Pro* plan or higher (required for managed OIDC), with the [Render CLI](https://render.com/docs/cli) installed and logged in
- Completed the [Call External APIs from MCP](/guides/delegated-access/index.md) guide: a working MCP server with delegated access and an Application in Keycard Console
- Your MCP server code in a Git repository that Render can access

## Create the Render service

1. **Deploy the service**

   Terminal window

   ```
   render services create \
     --name my-mcp-server \
     --type web_service \
     --repo <your-repo-url> \
     --branch main \
     --runtime python \
     --build-command "uv sync --frozen" \
     --start-command "uv run my-mcp-server" \
     --env-var PORT=10000 \
     --env-var KEYCARD_ISSUER=<keycard-issuer-url> \
     --env-var AWS_ROLE_ARN=keycard-oidc-trigger \
     -o json --confirm
   ```

   Find your **Issuer URL** in Keycard Console under **Settings** → **Connection**.

   Note

   The `AWS_ROLE_ARN` value is never used and your server never talks to AWS. Its *presence* is what tells Render to mount the OIDC token and set `AWS_WEB_IDENTITY_TOKEN_FILE` to its file path. This guide reuses the AWS trigger because the Keycard SDK discovers `AWS_WEB_IDENTITY_TOKEN_FILE` automatically.

2. **Note the service ID and URL**

   The create command returns the service ID (starts with `srv-`) and the public URL (`https://<your-service>.onrender.com`). You need both in the next sections.

3. **Set your server’s public URL**

   In the Render dashboard, add an environment variable to the service. This step uses the dashboard because the public URL only exists after creation, and the CLI’s update command does not manage environment variables:

   Terminal window

   ```
   MCP_SERVER_URL=https://<your-service>.onrender.com/mcp
   ```

4. **Register the production Resource**

   Keep the localhost Resource from the previous guide for local development. In Keycard Console, navigate to **Resources** → **Add Resource** → **Add Manually** and register the deployed server as a separate Resource:

   | Field                   | Value                                     |
   | ----------------------- | ----------------------------------------- |
   | **Resource Name**       | My MCP Server (Production)                |
   | **Resource Identifier** | `https://<your-service>.onrender.com/mcp` |
   | **Credential Provider** | Zone Provider                             |

## Keep your server code unchanged

No code change is required. One codebase serves both environments because the SDK selects the Application credential from the environment. Locally, `KEYCARD_CLIENT_ID` and `KEYCARD_CLIENT_SECRET` select your local Application’s client secret. On Render, those variables are absent, so the SDK discovers `AWS_WEB_IDENTITY_TOKEN_FILE` and authenticates as the production Application with the token it points to.

Your `AuthProvider` from the previous guide works unchanged:

```
import os


from keycardai.fastmcp import AuthProvider


auth_provider = AuthProvider(
    zone_url=os.environ["KEYCARD_ISSUER"],
    mcp_server_url=os.environ["MCP_SERVER_URL"],
)
```

The token is re-read on every request, so Render’s automatic rotation requires no handling in your code.

## Register the Render identity in Keycard

Rather than reusing the Application from the previous guide, create a second Application dedicated to the deployment. Each environment gets its own identity, its own dependencies, and its own audit attribution, and revoking one never touches the other.

1. **Create a Provider for Render’s OIDC issuer**

   In Keycard Console, navigate to **Providers** → **Add Provider**:

   | Field          | Value                                           |
   | -------------- | ----------------------------------------------- |
   | **Name**       | Render OIDC                                     |
   | **Identifier** | `https://oidc.render.com/<render-workspace-id>` |

   Your Render workspace ID starts with `tea-` and is shown at the top of the workspace’s **Settings** page in the Render dashboard. Keycard discovers Render’s signing keys from the issuer automatically.

2. **Create the production Application**

   Navigate to **Applications** → **Add Application**, name it (for example, My MCP Server Production), and click **Create Application**.

   Then, on the Application details page:

   - On the **Provides** tab, click **Add provided resource** and select the production Resource you registered earlier.
   - On the **Dependencies** tab, click **Add dependency** and select the same external API Resource(s) your local Application depends on.

   Do **not** generate client credentials for this Application. The next step gives it a credential that never needs storing.

3. **Add a workload identity credential**

   On the production Application’s details page, open **Credentials** and add a **Workload Identity** credential:

   | Field        | Value                                                                      |
   | ------------ | -------------------------------------------------------------------------- |
   | **Provider** | Render OIDC                                                                |
   | **Subject**  | `workspace:<render-workspace-id>:environment:default:service:<service-id>` |

   The subject must match the `sub` claim of your service’s token exactly. The environment segment is `default` unless the service belongs to a Render project environment, in which case it is the environment’s ID (starts with `evm-`). A trailing `*` wildcard is supported if you want to trust every service in the workspace, at the cost of looser pinning.

4. **Redeploy the service**

   Trigger a deploy so the service starts with the OIDC token mounted. Because the production Application authenticates only through workload identity, there are no `KEYCARD_CLIENT_ID` or `KEYCARD_CLIENT_SECRET` variables to set on the service.

## Verify

1. **Call a tool end to end**

   Connect an MCP client to `https://<your-service>.onrender.com/mcp`, complete the sign-in flow, and invoke a tool that calls the external API.

2. **Check the Audit Log**

   In Keycard Console, open the **Audit Log** and confirm `credentials:issue` events show the production Application authenticating with no client secret involved.

## Troubleshooting

### `invalid_client`: No token application credential configured

The error message includes the exact Provider issuer and subject Keycard extracted from your service’s token. Compare them against the Provider identifier and credential subject in Keycard Console. The subject must match character for character, including the service ID.

To inspect the token Render actually issued, SSH into the service and decode its claims:

Terminal window

```
render ssh <service-id>
cut -d. -f2 "$AWS_WEB_IDENTITY_TOKEN_FILE" | base64 -d
```

Confirm the `iss` and `sub` values match the Provider identifier and credential subject you registered.

### `AWS_WEB_IDENTITY_TOKEN_FILE` is not set

- Confirm `AWS_ROLE_ARN` is set on the service and redeploy, since Render mounts the token during deploys
- Managed OIDC requires a *Pro* workspace or higher

### Token exchange fails on Render but works locally

- Confirm `KEYCARD_CLIENT_ID` and `KEYCARD_CLIENT_SECRET` are not set on the service: a client secret takes priority over workload identity, so the service would authenticate as your local Application
- Verify the external API Resource is a **Dependency** on the production Application, not only on the local one
- Verify the production Resource’s **Resource Identifier** matches `https://<your-service>.onrender.com/mcp` exactly

### Dockerfile-based builds

Render does not provide the OIDC token at build time for services built from a Dockerfile. The token is available at runtime either way; if you need OIDC during builds, use a native runtime.

## What’s next

Your MCP server is running in production with no stored Keycard credential. From here you can extend what the deployed server does and watch how it is used.

- **[Act on Behalf of Absent Users](/guides/act-on-behalf-of-absent-users/index.md)**: run background work from the deployed server after a one-time user consent
- **[Audit Log & Sessions](/admin/audit-log-and-sessions/index.md)**: monitor every credential the production Application is issued
- **[Deploy an MCP server on Cloudflare Workers](/guides/cloudflare-worker/index.md)**: an alternative deployment target with its own credential options
