OpenClaw Module: Security Considerations¶
This document captures security decisions, hardening measures, and deviations from the default OpenClaw provisioning guide that were made to secure the deployment behind AWS ALB + Cognito.
Authentication Architecture¶
OpenClaw's default setup uses a gateway token (auth.mode = "token") where every client (browser, CLI) must present a shared secret to connect over WebSocket. This works for single-user local installations but breaks in a multi-user ALB-proxied deployment:
- The browser UI has no way to obtain the token automatically.
- Embedding the token in the page would defeat the purpose.
What we changed¶
We use auth.mode = "trusted-proxy" combined with AWS Cognito authentication at the ALB layer:
- Cognito handles user authentication (login, MFA, password policy).
- ALB listener rule enforces
authenticate-cognitoaction at priority 1, before the default website-pod rule. - OpenClaw trusts the ALB as a reverse proxy via
gateway.auth.trustedProxyand reads the user identity from thex-amzn-oidc-identityheader.
Configuration in locals.tf:
auth = {
mode = "trusted-proxy"
trustedProxy = {
userHeader = "x-amzn-oidc-identity"
}
}
trustedProxies = [for s in data.aws_subnet.alb : s.cidr_block]
Why this is secure¶
- Security groups: The
website-podmodule restricts backend EC2 ingress to only the ALB's security group (referenced_security_group_id). No direct access to port 5173 is possible from outside the ALB. - Trusted proxy CIDRs: OpenClaw only trusts proxy headers (
X-Forwarded-For,x-amzn-oidc-identity) from the ALB subnet CIDRs. These CIDRs are derived automatically fromdata.aws_subnet.alb, not hardcoded. - Header forgery protection: Even if someone reaches the EC2 instance (they can't — see point 1), they would need to send requests from an ALB subnet IP for OpenClaw to trust the proxy headers.
ALB headers available after Cognito authentication¶
| Header | Content | Signed? |
|---|---|---|
x-amzn-oidc-data | JWT with user claims (sub, email, name) | Yes (ES256) |
x-amzn-oidc-identity | Cognito user sub (UUID) | No |
x-amzn-oidc-accesstoken | OAuth2 access token | No |
We use x-amzn-oidc-identity because OpenClaw reads the header as a plain string identifier. The x-amzn-oidc-data JWT is cryptographically verifiable but OpenClaw does not perform JWT validation — it relies on the trusted proxy model instead.
Supply Chain Hardening¶
The default OpenClaw docs and many guides use curl | sh patterns for installing dependencies. We replaced all of them:
Node.js¶
Default guide: curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
Our approach: APT repository via cloud-init extra_repos with GPG key verification:
extra_repos = {
nodesource = {
source = "deb [signed-by=$KEY_FILE] https://deb.nodesource.com/node_22.x nodistro main"
keyid = "6F71F525282841EEDAF851B42F59B5F99B1BE0B4"
}
}
The GPG key is verified by fingerprint. APT will refuse packages that don't match the signing key.
Ollama¶
Default guide: curl -fsSL https://ollama.com/install.sh | sh
Our approach: Direct binary download from the official tarball, with manual systemd unit creation:
curl -fsSL -o "$OLLAMA_TMP/ollama.tar.zst" \
https://ollama.com/download/ollama-linux-amd64.tar.zst
tar --use-compress-program=unzstd -xf "$OLLAMA_TMP/ollama.tar.zst" -C "$OLLAMA_TMP"
install -m 755 "$OLLAMA_TMP/bin/ollama" /usr/local/bin/ollama
This avoids executing an arbitrary shell script as root. The systemd unit is written inline in the setup script with locked-down settings (dedicated ollama system user, no login shell).
OpenClaw¶
Our approach: Local npm install under the openclaw user (no root):
su - openclaw -c 'mkdir -p ~/openclaw-app && cd ~/openclaw-app && npm init -y && npm install openclaw'
No npm install -g (which requires root access to /usr/lib/node_modules). The systemd unit points to the local binary at /home/openclaw/openclaw-app/node_modules/.bin/openclaw.
Secrets Management¶
API keys (Anthropic, OpenAI)¶
Stored in AWS Secrets Manager via the infrahouse/secret/aws module, which provides KMS encryption and IAM-scoped read access. The instance profile is granted secretsmanager:GetSecretValue only for the specific secret ARN.
The setup script reads secrets at boot via ih-secrets (from infrahouse-toolkit). If the secret has not been populated yet (returns NoValue), the script logs a warning and continues — the service starts without those providers.
Gateway token (removed)¶
Originally planned to store a gateway token in Secrets Manager. This was removed when we switched to trusted-proxy auth mode. The gateway no longer requires a shared secret — Cognito + ALB handle authentication.
Systemd Hardening¶
The openclaw.service unit uses systemd security features:
| Directive | Effect |
|---|---|
NoNewPrivileges=true | Process cannot gain new privileges via setuid/setgid |
ProtectSystem=strict | Entire filesystem is read-only except allowed paths |
ProtectHome=tmpfs | All home directories are hidden behind an empty tmpfs |
BindPaths=/home/openclaw | Only /home/openclaw is bind-mounted into the namespace |
PrivateTmp=true | Private /tmp and /var/tmp |
User=openclaw | Runs as a dedicated unprivileged user |
Deviation from default: The default OpenClaw install guide uses ProtectHome=read-only, which exposes all home directories. We use ProtectHome=tmpfs + BindPaths to isolate the service to only its own home directory.
Network Security¶
ALB ingress¶
var.allowed_cidrs defaults to ["0.0.0.0/0"] — the ALB is publicly accessible. This is intentional: Cognito authentication is the access control layer, not network restriction. The ALB is the only entry point.
Backend isolation¶
The website-pod module creates a backend security group that only allows ingress from the ALB's security group:
resource "aws_vpc_security_group_ingress_rule" "backend_user_traffic" {
security_group_id = aws_security_group.backend.id
ip_protocol = "-1"
referenced_security_group_id = aws_security_group.alb.id
}
No CIDR-based rules for application traffic. The EC2 instance is unreachable except through the ALB.
EFS encryption¶
The EFS filesystem (persistent agent data at /home/openclaw/.openclaw) is encrypted at rest using the AWS-managed aws/elasticfilesystem KMS key. NFS ingress to the EFS mount targets is restricted to the backend subnet CIDRs.
WebSocket origin control¶
OpenClaw's controlUi.allowedOrigins is set to ["https://<fqdn>"], derived from the Route53 zone and DNS A records. This prevents cross-origin WebSocket connections from unauthorized domains.
Cognito Hardening¶
| Feature | Setting | Rationale |
|---|---|---|
deletion_protection | ACTIVE (configurable via enable_deletion_protection) | Prevent accidental pool deletion |
advanced_security_mode | ENFORCED | Detect compromised credentials |
mfa_configuration | OPTIONAL | Users can enable TOTP MFA |
allow_admin_create_user_only | true | No self-registration |
password_policy.minimum_length | 12 | Strong passwords required |
temporary_password length | 24 | Margin above minimum; users must change on first login |
Temporary passwords are stored in Terraform state. This is acceptable because: - State is stored in an encrypted S3 bucket with restricted access. - Users must change the password on first login. - allow_admin_create_user_only prevents registration bypass.
Logging and Auditability¶
- CloudWatch Logs: journald entries for
openclaw.serviceandollama.serviceare forwarded to a CloudWatch log group with 365-day retention (ISO 27001 / SOC 2 compliance). - Setup script logging: Every step logs with
[setup-openclaw]prefix and timestamp for post-mortem debugging viacloud-init-output.log. - Config validation: The setup script validates
openclaw.jsonwithjq emptybefore starting the service, failing fast with the file contents on error.
Summary of Deviations from Default OpenClaw Setup¶
| Area | Default guide | Our approach | Why |
|---|---|---|---|
| Authentication | Shared gateway token | Cognito + ALB + trusted-proxy | Multi-user, no token in browser |
| Node.js install | curl \| bash | APT repo with GPG key | Supply chain protection |
| Ollama install | curl \| sh | Direct binary tarball | No arbitrary script execution |
| OpenClaw install | npm install -g (root) | Local npm install (user) | Least privilege |
Systemd ProtectHome | read-only | tmpfs + BindPaths | Filesystem isolation |
| Config secrets | Inline in config file | Secrets Manager + env vars | No plaintext in userdata |
| Gateway bind | loopback | lan + trustedProxies | ALB needs network access |
| Proxy trust | Not configured | ALB subnet CIDRs (auto-derived) | Header forgery protection |