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.
On-premises deployment is available on Enterprise plans. To compare deployment models before you install, see Enterprise deployment.
Prerequisites
| Requirement | Details |
|---|---|
| Permit Platform deployed | Install 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 kubectl | Configured for your cluster. |
| Docker | Installed on the machine where you load images and push them to your registry. |
| Private container registry | Hosts the MCP Gateway images, such as Google Artifact Registry, Amazon ECR, or Harbor. |
| TLS certificate | For your MCP Gateway domain. The sign-in flow requires HTTPS. |
| DNS | You 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:
| Item | Example | Used for |
|---|---|---|
| Permit Platform URL | https://permit.yourcompany.com | Permit API URL and OpenID Connect (OIDC) discovery URL |
| MCP Gateway domain | mcp.yourcompany.com | Base domain for host subdomains, and the admin dashboard host (app.mcp.yourcompany.com) |
| Keycloak admin password | Retrieved from a Kubernetes secret | Automatic 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:
| Destination | Required | Purpose |
|---|---|---|
| Your Permit Platform URL | Required | Permit API for authorization, and Keycloak for OIDC |
| Your PDP URL (set per host in the admin dashboard) | Required | Policy decision point (PDP) for tool-level authorization |
| Your container registry | Install time only | Image pulls during deployment |
| Your identity provider (IdP) or OIDC discovery URL | Required | Admin dashboard sign-in (server-side token exchange) |
| Upstream MCP servers that you configure | Required | The 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:
- Loads the images from the bundled tarball into your local Docker.
- Retags each image with your registry prefix.
- Pushes the images to your registry.
- Replaces the
REGISTRYplaceholder invalues.yamlwith 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
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.
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:
| Placeholder | Replace 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.
| Option | URL | When to use |
|---|---|---|
| Internal Kubernetes DNS | http://permitio-pdp.<namespace>.svc.cluster.local:7766 | The PDP and the gateway run in the same cluster (recommended) |
| External URL | https://pdp.yourcompany.com | The 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.
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:
| Key | Set to |
|---|---|
global.baseDomain | Your MCP Gateway domain. Hosts get subdomains of this domain. |
global.permit.apiUrl | Your Permit Platform URL. |
permitPlatform | The 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.host | app. followed by your MCP Gateway domain. |
platform.oidc.discoveryUrl | Your 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:
| Record | Type | Value |
|---|---|---|
*.mcp.yourcompany.com | A | <ingress-ip> |
app.mcp.yourcompany.com | A | <ingress-ip> |
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
-
Check that the pods are running:
kubectl get pods -n agent-securityWith the replica counts in the default on-premises values template, you see 10 pods, each
1/1 Running:Component Pods What it does Gateway 2 MCP proxy that enforces authentication and authorization Consent service 2 OAuth 2.1 authorization server Platform 2 Admin dashboard NGINX (OpenResty) 2 Reverse proxy that routes requests PostgreSQL 1 User and session storage Redis 1 Gateway 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. -
Check the ingresses:
kubectl get ingress -n agent-securityYou see two ingresses:
Name Host Purpose agent-security-gateway*.mcp.yourcompany.comTraffic to gateway hosts agent-security-platformapp.mcp.yourcompany.comAdmin dashboard -
Check the admin dashboard health endpoint:
curl -s https://app.mcp.yourcompany.com/api/health# Expected: {"status":"healthy"}The response is
{"status":"healthy"}. -
Open
https://app.mcp.yourcompany.comin 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"
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
| Symptom | Cause | Fix |
|---|---|---|
helm install fails | A pod or Job failed to start | See Install failed |
| Gateway pods restart on first deploy | Redis is still starting | No action. The gateway reconnects after 1 or 2 restarts. |
| Admin dashboard sign-in returns to the sign-in page | No TLS on the ingresses, so the browser drops the secure cookies | Attach the TLS secret |
| "Invalid credentials" when creating an organization | The Keycloak OIDC client is missing the audience mapper | See Invalid credentials when creating an organization |
Pods stuck in CreateContainerConfigError | The Keycloak setup Job hasn't created the OIDC secret yet | Wait 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 errors | PostgreSQL isn't ready yet | See 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
- Set up hosts for your organization: create your first host, set its PDP URL, and import MCP servers.
- Authentication methods: choose how users sign in to the consent service.
- Getting started with Permit MCP Gateway: connect MCP clients and test allowed and denied tool calls.
- Architecture: the components you installed and how requests flow between them.