Skip to main content

Install Permit MCP Gateway on-premises

Install Permit MCP Gateway with Helm in your own Kubernetes cluster, connected to your self-hosted Permit Platform for authentication and authorization. This page is for platform operators who run a fully on-premises deployment. When you finish, the gateway, the consent service, and the admin dashboard run in your cluster, and you can sign in to the admin dashboard on your own domain.

Enterprise plans

On-premises deployment is available on Enterprise plans. To compare deployment models before you install, see Enterprise deployment.

Prerequisites

RequirementDetails
Permit Platform deployedInstall the Permit Platform first, with the Permit Platform installation guide. The Permit Platform provides Keycloak (authentication) and the Permit backend (authorization).
Kubernetes cluster (1.25+)With an NGINX ingress controller installed. Check the README.md in your installer package for the supported versions of your release.
Helm 3.x and kubectlConfigured for your cluster.
DockerInstalled on the machine where you load images and push them to your registry.
Private container registryHosts the MCP Gateway images, such as Google Artifact Registry, Amazon ECR, or Harbor.
TLS certificateFor your MCP Gateway domain. The sign-in flow requires HTTPS.
DNSYou can create DNS records for your MCP Gateway domain: a wildcard record and a record for the admin dashboard.

Information you'll need

The installation uses three values:

ItemExampleUsed for
Permit Platform URLhttps://permit.yourcompany.comPermit API URL and OpenID Connect (OIDC) discovery URL
MCP Gateway domainmcp.yourcompany.comBase domain for host subdomains, and the admin dashboard host (app.mcp.yourcompany.com)
Keycloak admin passwordRetrieved from a Kubernetes secretAutomatic creation of the OIDC client in Keycloak

Retrieve the Keycloak admin password from your Permit Platform cluster. Replace <permit-platform-namespace> with the namespace where the Permit Platform runs:

kubectl get secret global-infrastructure-secret \
-n <permit-platform-namespace> \
-o jsonpath='{.data.KEYCLOAK_ADMIN_PASSWORD}' | base64 -d

Network egress requirements

The gateway runs fully on-premises. This table lists each outbound connection the gateway components can make:

DestinationRequiredPurpose
Your Permit Platform URLRequiredPermit API for authorization, and Keycloak for OIDC
Your PDP URL (set per host in the admin dashboard)RequiredPolicy decision point (PDP) for tool-level authorization
Your container registryInstall time onlyImage pulls during deployment
Your identity provider (IdP) or OIDC discovery URLRequiredAdmin dashboard sign-in (server-side token exchange)
Upstream MCP servers that you configureRequiredThe gateway proxies tool calls to upstream MCP servers

Normal operation doesn't require a connection to api.permit.io, app.permit.io, or any other external cloud service. The on-premises values template turns off external analytics and telemetry integrations.

Install the gateway

1. Extract the installer package

Your Permit team sends you a .tar.gz installer package. Extract the package:

tar -xzf agent-security-on-prem-installer-*.tar.gz
cd agent-security-on-prem-installer-*

The package contains:

agent-security-on-prem-installer-*/
├── charts/agent-security/ # Helm chart
├── images/all-images.tar.gz # Docker images (bundled for air-gapped use)
├── scripts/
│ ├── load-images.sh # Push images to your registry
│ └── setup-keycloak.sh # Manual Keycloak setup (for debugging)
├── values.yaml # On-prem values template
└── README.md # Quick reference

2. Push the images to your registry

Authenticate to your container registry, then run the image loader. Replace <your-registry> with your registry path:

# Authenticate (example for GCP)
gcloud auth configure-docker us-central1-docker.pkg.dev

# Push all images and update values.yaml with your registry paths
./scripts/load-images.sh --registry <your-registry>

# See all options
./scripts/load-images.sh --help

Examples for common registries:

# Google Artifact Registry
./scripts/load-images.sh --registry us-central1-docker.pkg.dev/my-project/mcp-gateway

# AWS ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
./scripts/load-images.sh --registry 123456789.dkr.ecr.us-east-1.amazonaws.com/mcp-gateway

# Harbor
./scripts/load-images.sh --registry harbor.company.com/mcp-gateway

The load-images.sh script:

  1. Loads the images from the bundled tarball into your local Docker.
  2. Retags each image with your registry prefix.
  3. Pushes the images to your registry.
  4. Replaces the REGISTRY placeholder in values.yaml with your registry.

3. Choose how to pass the Keycloak admin password

During installation, a Helm post-install Job creates the Keycloak OIDC client for the admin dashboard. You don't set up Keycloak by hand. The Job needs the Keycloak admin password from Information you'll need. You add one of these two options to my-values.yaml in step 6.

Option A: reference the existing secret (recommended)

permitPlatform:
namespace: "<permit-platform-namespace>"
keycloakAdminPasswordSecret: "global-infrastructure-secret"
keycloakAdminPasswordSecretKey: "KEYCLOAK_ADMIN_PASSWORD"

Option B: plaintext value

Plaintext passwords in values files

With Option B, the Keycloak admin password is stored in plain text in my-values.yaml. Anyone who can read that file can sign in to Keycloak as an admin. Use Option A, or keep my-values.yaml out of version control.

permitPlatform:
namespace: "<permit-platform-namespace>"
keycloakAdminPassword: "<password>"

The Keycloak setup Job is idempotent, so it runs safely on each helm upgrade. The Job never recreates an existing OIDC client.

Admin dashboard pod waits for the Keycloak setup Job

Until the Keycloak setup Job creates the OIDC secret, the admin dashboard (platform) pod shows CreateContainerConfigError. This lasts about 30 to 60 seconds, and the pod recovers without action.

Manual Keycloak setup (for debugging only)

If the Helm Job can't reach Keycloak, for example because network policies block traffic between namespaces, run the standalone setup script instead. Run the script only if the Helm Job fails, and don't run both.

./scripts/setup-keycloak.sh --agent-security-domain mcp.yourcompany.com
./scripts/setup-keycloak.sh --help # See all options

4. Deploy a PDP

The gateway sends each authorization check to a policy decision point (PDP). If you don't have a PDP deployed, install one with the Permit PDP Helm chart:

helm repo add pdp https://permitio.github.io/pdp-helm
helm repo update

helm install pdp pdp/pdp \
--set pdp.ApiKey="<YOUR_PERMIT_API_KEY>" \
--set "pdp.pdpEnvs[0].name=PDP_CONTROL_PLANE" \
--set "pdp.pdpEnvs[0].value=<PERMIT_BACKEND_INTERNAL_URL>" \
--namespace <permit-platform-namespace>

Replace these placeholders:

PlaceholderReplace with
<YOUR_PERMIT_API_KEY>The environment API key from your Permit Platform dashboard. See Get your API key.
<PERMIT_BACKEND_INTERNAL_URL>The Permit backend service URL inside your cluster, such as http://permit-backend-v2.<namespace>.svc.cluster.local:8000
<permit-platform-namespace>The namespace where the Permit Platform runs

Note the PDP service URL. You enter the PDP URL for each host in the admin dashboard when you create hosts.

PDP URL is set per host in the admin dashboard

The Helm values don't set a global PDP URL. Each host has its own PDP URL in the admin dashboard, so different hosts can use different PDPs.

OptionURLWhen to use
Internal Kubernetes DNShttp://permitio-pdp.<namespace>.svc.cluster.local:7766The PDP and the gateway run in the same cluster (recommended)
External URLhttps://pdp.yourcompany.comThe PDP is exposed through an ingress or runs in a different cluster

The PDP Helm chart names its service permitio-pdp and listens on port 7766 by default.

One PDP per environment or tenant

You can deploy several PDPs, such as one per environment or per tenant, and give each host a different PDP URL. Each host uses its own PDP.

5. Create the TLS secret

The sign-in flow requires HTTPS because its cookies are set with secure: true. Create a TLS secret from your certificate and key files:

kubectl create secret tls agent-security-tls \
--cert=path/to/tls.crt \
--key=path/to/tls.key \
-n agent-security --create-namespace

To manage certificates automatically instead, use cert-manager with annotations on your ingresses.

6. Configure values

values.yaml already has your registry paths from step 2. Copy values.yaml to a file for your settings:

cp values.yaml my-values.yaml

Open my-values.yaml and set the required values:

global:
# Your MCP Gateway domain: tenants will be at <name>.mcp.yourcompany.com
baseDomain: "mcp.yourcompany.com"
permit:
# Your Permit Platform URL
apiUrl: "https://permit.yourcompany.com"
# PDP URL is set per-host in the Platform UI (not here)

# Keycloak integration (automatic: just provide the password)
permitPlatform:
namespace: "<permit-platform-namespace>"
keycloakAdminPassword: "<keycloak-admin-password>"

# OpenResty reverse proxy (required for on-prem)
nginx:
enabled: true

# Platform UI
platform:
ingress:
host: "app.mcp.yourcompany.com"
oidc:
providerId: "keycloak"
providerName: "Keycloak"
discoveryUrl: "https://permit.yourcompany.com/auth/realms/permit-platform/.well-known/openid-configuration"
clientId: "agent-security-platform"
existingSecret: "agent-security-oidc-secret"
existingSecretKey: "OIDC_CLIENT_SECRET"

Set these values for your installation:

KeySet to
global.baseDomainYour MCP Gateway domain. Hosts get subdomains of this domain.
global.permit.apiUrlYour Permit Platform URL.
permitPlatformThe Keycloak password option you chose in step 3. The template shows Option B. For Option A, replace the keycloakAdminPassword line with the keycloakAdminPasswordSecret and keycloakAdminPasswordSecretKey lines.
platform.ingress.hostapp. followed by your MCP Gateway domain.
platform.oidc.discoveryUrlYour Permit Platform URL followed by /auth/realms/permit-platform/.well-known/openid-configuration.

Keep nginx.enabled: true. The gateway ingress routes traffic through the bundled OpenResty reverse proxy.

The chart sets these values for you:

  • Keycloak OIDC client: created by the Helm Job during installation.
  • Database passwords: generated, and kept across upgrades.
  • Redis password: generated.
  • Admin tokens and session secrets: generated.
  • Email verification: off by default for on-premises deployments. To turn it on, see Email notifications.

7. Install the Helm chart

helm install agent-security ./charts/agent-security \
-f my-values.yaml \
-n agent-security \
--create-namespace --wait --timeout=10m

The command returns when all resources are ready, or fails after 10 minutes. If it fails, see Install failed.

8. Configure DNS

Get the external IP address of your ingress:

# Get the ingress controller IP
kubectl get ingress -n agent-security

Add these DNS records with your DNS provider, using the IP address from the ADDRESS column:

RecordTypeValue
*.mcp.yourcompany.comA<ingress-ip>
app.mcp.yourcompany.comA<ingress-ip>
Add an explicit record for the admin dashboard

With some DNS providers, the wildcard record doesn't resolve app.mcp.yourcompany.com. If host subdomains work but the admin dashboard is unreachable, check that the explicit app.mcp.yourcompany.com record exists.

9. Attach the TLS secret to the ingresses

If you don't use cert-manager, attach the TLS secret from step 5 to both ingresses. Replace mcp.yourcompany.com with your MCP Gateway domain:

kubectl patch ingress agent-security-platform -n agent-security --type=json \
-p='[{"op":"add","path":"/spec/tls","value":[{"hosts":["app.mcp.yourcompany.com"],"secretName":"agent-security-tls"}]}]'

kubectl patch ingress agent-security-gateway -n agent-security --type=json \
-p='[{"op":"add","path":"/spec/tls","value":[{"hosts":["*.mcp.yourcompany.com"],"secretName":"agent-security-tls"}]}]'

Without TLS on the ingresses, admin dashboard sign-in fails and returns you to the sign-in page.

Verify the installation

  1. Check that the pods are running:

    kubectl get pods -n agent-security

    With the replica counts in the default on-premises values template, you see 10 pods, each 1/1 Running:

    ComponentPodsWhat it does
    Gateway2MCP proxy that enforces authentication and authorization
    Consent service2OAuth 2.1 authorization server
    Platform2Admin dashboard
    NGINX (OpenResty)2Reverse proxy that routes requests
    PostgreSQL1User and session storage
    Redis1Gateway state

    If you changed replica counts in my-values.yaml, or use an external database, the pod count differs. On the first deploy, gateway pods can show 1 or 2 restarts while Redis starts. The gateway reconnects without action.

  2. Check the ingresses:

    kubectl get ingress -n agent-security

    You see two ingresses:

    NameHostPurpose
    agent-security-gateway*.mcp.yourcompany.comTraffic to gateway hosts
    agent-security-platformapp.mcp.yourcompany.comAdmin dashboard
  3. Check the admin dashboard health endpoint:

    curl -s https://app.mcp.yourcompany.com/api/health
    # Expected: {"status":"healthy"}

    The response is {"status":"healthy"}.

  4. Open https://app.mcp.yourcompany.com in your browser. The admin dashboard sign-in page shows a Sign in with Keycloak button.

Configuration options

Use an external PostgreSQL and Redis

To use your own PostgreSQL and Redis instead of the bundled ones, add these values to my-values.yaml. Each existingSecret names a Kubernetes secret in the agent-security namespace, and existingSecretKey names the key that holds the connection URL:

postgres:
enabled: false
externalDatabase:
consent:
existingSecret: "my-postgres-secret"
existingSecretKey: "DATABASE_URL"
platform:
existingSecret: "my-postgres-secret"
existingSecretKey: "DATABASE_URL"

redis:
enabled: false
externalRedis:
enabled: true
host: "redis.yourcompany.com"
port: 6379
existingSecret: "my-redis-secret"
existingSecretKey: "REDIS_URL"

Storage

To set the storage class and volume size for the bundled PostgreSQL and Redis:

postgres:
persistence:
storageClass: "your-storage-class"
size: 20Gi
redis:
persistence:
storageClass: "your-storage-class"
size: 2Gi

Email notifications

Email verification is off by default. To use email for registration, password reset, and one-time passcodes (OTP), configure an SMTP server. existingSecret names a Kubernetes secret that holds the SMTP password:

email:
provider: "smtp"
from: "MCP Gateway <noreply@yourcompany.com>"
requireVerification: "true"
smtp:
host: "smtp.yourcompany.com"
port: 587
username: "noreply@yourcompany.com"
existingSecret: "my-smtp-secret"
existingSecretKey: "SMTP_PASSWORD"
SMTP only in on-premises mode

The mailgun email provider isn't available in on-premises mode, so email can't be sent through an external cloud service by mistake.

Permit Platform in a different cluster

If the Permit Platform runs in a different cluster, the Keycloak setup Job can't find Keycloak through Kubernetes DNS. Set the Keycloak URL:

permitPlatform:
keycloakUrl: "https://permit.yourcompany.com/auth"
keycloakAdminPassword: "<password>" # or use keycloakAdminPasswordSecret (see Step 3)

Upgrade the gateway

Run helm upgrade with the same values file:

helm upgrade agent-security ./charts/agent-security \
-f my-values.yaml \
-n agent-security \
--wait --timeout=10m

Upgrades keep the generated secrets and the data in PostgreSQL and Redis. Run the verification checks after each upgrade.

If an upgrade fails and pods are unhealthy, roll back to the previous release:

helm rollback agent-security -n agent-security --wait --timeout=10m

Troubleshooting

SymptomCauseFix
helm install failsA pod or Job failed to startSee Install failed
Gateway pods restart on first deployRedis is still startingNo action. The gateway reconnects after 1 or 2 restarts.
Admin dashboard sign-in returns to the sign-in pageNo TLS on the ingresses, so the browser drops the secure cookiesAttach the TLS secret
"Invalid credentials" when creating an organizationThe Keycloak OIDC client is missing the audience mapperSee Invalid credentials when creating an organization
Pods stuck in CreateContainerConfigErrorThe Keycloak setup Job hasn't created the OIDC secret yetWait 30 to 60 seconds. If the state persists, check the logs of the Keycloak setup Job.
Consent service or platform pods fail to start with migration errorsPostgreSQL isn't ready yetSee Database migration errors

Install failed

If helm install fails, find the failed pod or Job, fix the cause, then uninstall and install again. Uninstalling keeps the persistent volume claims (PVCs), so your data stays.

# Check what failed
kubectl get pods -n agent-security
kubectl get jobs -n agent-security

# Uninstall (PVCs are retained, your data is safe)
helm uninstall agent-security -n agent-security

# Re-run install after fixing the issue
helm install agent-security ./charts/agent-security \
-f my-values.yaml -n agent-security \
--create-namespace --wait --timeout=10m

Gateway pods restarting

On the first deploy, Redis takes a few seconds to start. The gateway reconnects after 1 or 2 restarts, and no action is needed.

Platform login redirects back to login page

The ingresses have no TLS. The sign-in flow sets secure cookies, and the browser sends secure cookies only over HTTPS. Attach the TLS secret to the ingresses.

"Invalid credentials" when creating an organization

The Keycloak OIDC client might be missing the required audience mapper. Check the logs of the Keycloak setup Job:

kubectl logs -n agent-security -l app.kubernetes.io/component=keycloak-setup

If the Job failed, run helm upgrade with the same values to run the Job again.

Pods stuck in CreateContainerConfigError

The OIDC secret doesn't exist yet. The Keycloak setup Job runs after the main resources deploy. The platform pod recovers when the Job completes, usually within 30 to 60 seconds.

Database migration errors

Consent service or platform pods fail to start if PostgreSQL isn't ready yet. The migration init containers retry on their own. To check the migration logs:

kubectl logs -n agent-security deployment/agent-security-consent-service -c db-migration
kubectl logs -n agent-security deployment/agent-security-platform -c db-migration

Checking logs

To read recent logs for each component:

# Gateway
kubectl logs -n agent-security deployment/agent-security-gateway --tail=20

# Consent Service
kubectl logs -n agent-security deployment/agent-security-consent-service --tail=20

# Platform
kubectl logs -n agent-security deployment/agent-security-platform --tail=20

# Keycloak setup Job
kubectl logs -n agent-security -l app.kubernetes.io/component=keycloak-setup

Next steps