Engineering

KMS Encryption, Read-Only Filesystems, and Zero Inbound Ports: Inside Alpha Agent's Security Model

A technical deep dive into how Alpha Agent encrypts secrets with AWS KMS, enforces immutable containers, and eliminates attack surface through zero inbound port architecture.

Bradley Taylor ·
Updated February 20, 2026

AI agents hold your most sensitive credentials

An AI agent is not a chatbot. It holds API keys for your AI providers, OAuth tokens for Slack and Discord, session secrets, and in many cases, credentials that grant write access to production systems. A compromised agent does not just leak conversation history — it provides an attacker with authenticated access to every service the agent is connected to.

This is the threat model that drives every security decision in Alpha Agent. We assume that containers will be targeted, that network probes will happen, and that any secret stored in plaintext is a secret already stolen. The question is not whether an attacker will try. The question is what they can do when they get in.

This post covers the three pillars of Alpha Agent’s security architecture: KMS-encrypted secrets at rest and in transit, read-only container filesystems with immutable root images, and a zero inbound port network model that eliminates the most common attack vector entirely.

KMS encryption: secrets never exist in plaintext

The problem with configuration files

Self-hosted OpenClaw stores secrets in openclaw.json and .env files on the local filesystem. API keys, bot tokens, and OAuth credentials sit in plaintext, readable by any process running as the same user. If the host is compromised — through an exposed port, an unpatched vulnerability, or a stolen SSH key — every secret is immediately available. This is precisely the attack pattern that infostealer campaigns targeting AI agents exploit: once malware gains filesystem access, plaintext config files are the first thing it reads.

Alpha Agent takes a fundamentally different approach. Per-user secrets are encrypted with AWS KMS before they are written to storage, and they are only decrypted at the moment they are needed by the provisioning system.

The encryption flow

When a user configures an integration — adding an Anthropic API key, connecting a Slack workspace, or setting up an OAuth flow — the management Lambda encrypts each secret individually using a dedicated KMS key before persisting it to DynamoDB:

import { KMSClient, EncryptCommand, DecryptCommand } from '@aws-sdk/client-kms';

const kms = new KMSClient({ region: 'us-east-1' });

async function encryptSecret(plaintext) {
  const { CiphertextBlob } = await kms.send(new EncryptCommand({
    KeyId: KMS_KEY_ARN,
    Plaintext: Buffer.from(plaintext, 'utf-8'),
  }));
  return Buffer.from(CiphertextBlob).toString('base64');
}

async function decryptSecret(ciphertext) {
  const { Plaintext } = await kms.send(new DecryptCommand({
    CiphertextBlob: Buffer.from(ciphertext, 'base64'),
  }));
  return Buffer.from(Plaintext).toString('utf-8');
}

Each secret is stored as an individual encrypted field in the user’s DynamoDB record under the encrypted_secrets map. This means rotating a single key does not require re-encrypting all of a user’s secrets, and different secrets can have different lifecycle management without touching the rest.

Scoped IAM policies

The KMS key is not a general-purpose encryption key. The IAM policy on the management Lambda role restricts it to exactly three operations on exactly one key:

- PolicyName: alphaagent-kms
  PolicyDocument:
    Version: '2012-10-17'
    Statement:
      - Effect: Allow
        Action:
          - kms:Encrypt
          - kms:Decrypt
          - kms:GenerateDataKey
        Resource: !Ref KMSKeyArn

No other service, role, or user in the AWS account can use this key. The Lambda function is the only entity that can encrypt or decrypt user secrets, and it only does so in response to authenticated API requests that have passed Auth0 JWT validation and Stripe subscription verification.

Secrets in transit to the container

When a container is provisioned, the Lambda decrypts each secret from DynamoDB, assembles a .env file, and writes it to S3 with server-side KMS encryption:

await s3.send(new PutObjectCommand({
  Bucket: S3_BUCKET,
  Key: `containers/${slug}/.env`,
  Body: envLines.join('\n'),
  ServerSideEncryption: 'aws:kms',
  SSEKMSKeyId: KMS_KEY_ARN,
}));

The .env file is encrypted at rest in S3, downloaded to the host during provisioning via an authenticated IAM role, and mounted into the container with restrictive permissions (chmod 600). At no point does a secret exist in plaintext on a network or in an unencrypted store.

Why not AWS Secrets Manager for per-user secrets?

AWS Secrets Manager charges $0.40 per secret per month plus $0.05 per 10,000 API calls. For a service that stores 5-10 secrets per user, that adds up to $3.15/user/month in Secrets Manager costs alone. KMS encryption of DynamoDB fields provides the same AES-256 encryption standard at a fraction of the cost — $1/month for the KMS key plus negligible per-request charges, regardless of user count.

We use Secrets Manager for the approximately 10 shared infrastructure secrets (Auth0 client credentials, Stripe API keys, OAuth client secrets) where the per-secret cost is fixed and the operational benefits of automatic rotation are worth it. Per-user secrets go through KMS and DynamoDB, where the economics scale linearly with our user base.

For more details on our encryption approach, see our encryption documentation.

Read-only filesystems: immutable containers by default

What immutability prevents

A writable filesystem inside a container gives an attacker persistence. They can modify binaries, install backdoors, tamper with configuration files, or write malicious scripts that survive process restarts. Even without root access, a writable /usr/local/bin or /etc directory is enough to establish a foothold.

Alpha Agent containers run with the Docker read_only: true flag. The entire root filesystem is mounted as read-only by the container runtime:

services:
  oc-{SLUG}:
    image: 025066246735.dkr.ecr.us-east-1.amazonaws.com/alphaagent/openclaw:{IMAGE_TAG}
    read_only: true
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp:size=256M
    deploy:
      resources:
        limits:
          memory: 3072M
          cpus: '1.0'
        reservations:
          memory: 512M
          cpus: '0.25'

What is writable, and why

A completely frozen filesystem would prevent the application from functioning. Three categories of data need write access:

  1. /tmp (tmpfs, 256MB) — Temporary files for active processing. Stored in memory, never written to disk, and cleared when the container restarts.
  2. Workspace data (/home/agent/workspace/{slug}) — The user’s files, synced to S3. This is the user’s data, and they need write access to it.
  3. OpenClaw state (/home/agent/.openclaw) — Gateway configuration and runtime state. Mounted as a host volume scoped to the individual user.

Dashboard code and application binaries are bind-mounted as read-only from the host:

volumes:
  - /data/dashboard/server.js:/app/dashboard/server.js:ro
  - /data/dashboard/lib:/app/dashboard/lib:ro
  - /data/dashboard/routes:/app/dashboard/routes:ro
  - /data/dashboard/dist:/app/dashboard/dist:ro

An attacker who gains code execution inside the container cannot modify the dashboard, install packages, alter system libraries, or write to any path outside the three explicitly writable directories. If the container is restarted, any changes to /tmp are gone, and the root filesystem is back to its original state from the image.

No-new-privileges and the privilege escalation boundary

The no-new-privileges security option prevents processes inside the container from gaining additional capabilities through setuid or setgid binaries. This closes a class of privilege escalation attacks where an attacker finds a SUID binary and uses it to escalate from the agent user to root within the container.

Combined with the read-only filesystem, this means an attacker cannot write a SUID binary and cannot use existing ones to escalate. The container’s agent user remains the ceiling for any compromised process.

Read more about our container security controls at container isolation documentation.

Zero inbound ports: eliminating the attack surface

How most AI agents get compromised

The SecurityScorecard STRIKE team found over 135,000 exposed OpenClaw instances on the public internet, with 63% running vulnerable versions. The attack vector is straightforward: port 18789 (gateway) or port 18790 (dashboard) is reachable from the internet, often without authentication, and the attacker connects directly.

This is not a sophisticated attack. It is a port scan followed by an HTTP request. The entire category of attack disappears if there are no ports to scan.

Security group configuration

Alpha Agent EC2 instances have security groups with zero inbound rules. Not “restricted to certain IPs.” Not “limited to port 443.” Zero. There is no inbound rule in the security group at all.

User traffic reaches the containers through the ALB (Application Load Balancer), which terminates TLS and forwards requests to Nginx on the host. Nginx routes by subdomain to the correct container port. But the container ports are bound to 127.0.0.1 only — they are not accessible from outside the host:

ports:
  - "127.0.0.1:{DASHBOARD_PORT}:18790"
  - "127.0.0.1:{GATEWAY_PORT}:18789"

The ALB is the only entry point, and it only accepts HTTPS on port 443. There is no SSH, no direct container access, no management port, and no debug endpoint exposed to the network.

Management without SSH

If there are no inbound ports, how do we manage the instances? Through AWS Systems Manager (SSM) Session Manager. SSM uses an outbound-only agent running on the instance that maintains a persistent connection to the SSM service endpoint. Management sessions are initiated from the AWS console or CLI, authenticated through IAM, and logged in CloudTrail.

This eliminates an entire class of attacks:

  • No SSH key management — There are no SSH keys to steal, rotate, or accidentally commit to a repository.
  • No brute-force surface — There is no port 22 to probe.
  • Full audit trail — Every session is logged with the IAM identity that initiated it, the commands executed, and the session duration.

IMDS protection and SSRF prevention

Even with zero inbound ports, a server-side request forgery (SSRF) vulnerability in the application layer could allow an attacker to reach the EC2 Instance Metadata Service (IMDS) at 169.254.169.254 and steal IAM role credentials.

Alpha Agent blocks this at two layers. First, an iptables rule on the host blocks all IMDS traffic originating from Docker containers — this is the strongest possible control, preventing any container process from reaching the metadata endpoint regardless of IMDS version:

# Block IMDS access from Docker containers (prevents credential theft)
iptables -I DOCKER-USER -d 169.254.169.254 -j DROP

Second, the host itself uses IMDSv2 with session tokens for its own metadata requests, which requires a PUT request with a TTL header — something that standard SSRF payloads cannot perform. Even if an attacker achieves code execution inside a container, the iptables rule drops the traffic before it reaches the metadata endpoint. The host’s IAM credentials are invisible to containers.

For more on our network architecture, see our infrastructure security documentation.

Defense in depth: how the layers interact

No single control is sufficient. Security engineering is about layered controls where the failure of any one layer does not compromise the system. This principle — zero trust architecture — means every layer assumes the layers around it are already breached. Here is how the layers interact in a realistic attack scenario.

Scenario: Remote code execution inside a container

Assume an attacker exploits a vulnerability in OpenClaw’s gateway and achieves arbitrary code execution inside a user’s container.

  1. Read-only filesystem — The attacker cannot modify system binaries, install persistence mechanisms, or alter the application code. Any files written to /tmp are lost on restart.
  2. No-new-privileges — The attacker cannot escalate from the agent user to root. They are confined to the permissions of the application user.
  3. Network isolation — The container runs in its own Docker network. The attacker cannot reach other users’ containers, scan the host’s internal services, or access the metadata endpoint (blocked by iptables).
  4. Encrypted secrets — The secrets inside the container’s .env are the only secrets the attacker can access — and those are scoped to the single compromised user. The KMS key, DynamoDB table, and other users’ secrets are inaccessible from within the container.
  5. Zero inbound ports — The attacker cannot establish a reverse shell listener on the host. There are no open ports to bind to, and outbound traffic is the only path out.

The blast radius of a full container compromise is limited to a single user’s runtime environment. No lateral movement. No persistence. No privilege escalation. No access to infrastructure credentials.

What does enterprise-grade AI agent security cost?

Security architecture involves trade-offs. Container isolation with read-only filesystems requires careful volume planning. KMS encryption adds latency to provisioning (roughly 50-100ms per encrypt/decrypt call). SSM-only management means you cannot just “SSH in and check something” — you go through IAM authentication every time.

These are trade-offs we accept because the alternative — plaintext secrets, writable containers, and exposed ports — is not a trade-off. It is a liability. When your AI agent holds the keys to your Slack workspace, your AI provider account, and your team’s communication channels, “good enough” security is not good enough.

If you are evaluating AI agent platforms, ask three questions: Where are my secrets stored, and how are they encrypted? Can the container filesystem be modified at runtime? What ports are exposed to the internet? If the answers are “in a config file,” “yes,” and “at least two,” you have your answer. For a concrete look at what those answers mean in practice, see our localhost vs container comparison.