Complete configuration reference for all AppControl components.
- Architecture Overview
- Users & Authentication
- Backend (API Server)
- Gateway
- Agent
- Frontend
- CLI (appctl)
- TLS / mTLS Certificates
- Docker Compose Reference
- Production Checklist
┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Frontend │──HTTP──│ Backend (API) │──SQL───│ PostgreSQL 16 │
│ React SPA │ :8080 │ Rust + Axum │ :5432 │ │
│ (nginx) │ │ :3000 │ │ │
└─────────────┘ └────────┬─────────┘ └──────────────────┘
│ WebSocket
│ /ws/gateway
┌────────┴─────────┐
│ Gateway │
│ Rust + Axum │
│ :4443 │
└────────┬─────────┘
│ WebSocket (mTLS)
┌───────────┼───────────┐
┌────┴────┐ ┌───┴─────┐ ┌───┴─────┐
│ Agent 1 │ │ Agent 2 │ │ Agent N │
│ (host) │ │ (host) │ │ (host) │
└─────────┘ └─────────┘ └─────────┘
Key connectivity rules:
- Frontend → Backend: HTTP/HTTPS (reverse-proxied by nginx)
- Backend → PostgreSQL: TCP/TLS (SQL)
- Backend ← Gateway: WebSocket (the gateway initiates the connection)
- Gateway ← Agents: WebSocket with mTLS (agents initiate the connection)
- Agents never connect directly to the backend or database
- Gateways never connect to the database
AppControl supports three authentication methods. Only one needs to be active at a time, although OIDC and SAML can coexist.
| Method | Use Case | Configuration |
|---|---|---|
| Dev Login | Local development, quickstart | Automatic when APP_ENV=development |
| OIDC | Enterprise SSO (Keycloak, Okta, Azure AD, Google) | Set OIDC_DISCOVERY_URL |
| SAML 2.0 | Enterprise SSO (ADFS, Azure AD, Okta, Shibboleth) | Set SAML_IDP_SSO_URL |
When APP_ENV=development (the default), the backend automatically seeds a default admin user on first startup if the database is empty.
| Field | Value |
|---|---|
admin@localhost |
|
| Display name | Dev Admin |
| Role | admin (full access) |
| Organization | Dev Org |
| Password | (none — dev mode does not require a password) |
To log in via the UI: Open http://localhost:8080, enter admin@localhost in the email field, type any value in the password field (it is ignored in dev mode), and click Sign in.
To log in via the API:
TOKEN=$(curl -s -X POST http://localhost:3000/api/v1/auth/dev-login \
-H "Content-Type: application/json" \
-d '{"email":"admin@localhost"}' | jq -r '.token')Warning: The
dev-loginandloginendpoints are only available whenAPP_ENV=development. In production, they return404 Not Found— you must configure OIDC or SAML.
| Role | Scope | Description |
|---|---|---|
admin |
Organization-wide | Implicit owner on all applications. Can manage users, teams, and organization settings. |
viewer |
Default for new SSO users | Read-only access. Actual permissions depend on team memberships and per-app grants. |
Permissions are evaluated per application. The effective permission is the maximum of all grants:
view < operate < edit < manage < owner
| Level | Can do |
|---|---|
view |
See the application map, status, and logs |
operate |
Start, stop, restart, run diagnostics |
edit |
Modify components, commands, dependencies |
manage |
Manage permissions, share links |
owner |
Delete the application, transfer ownership |
Effective permission = MAX(direct_user_grant, all_team_grants). Organization admins have implicit owner on everything.
Teams group users and grant them permissions on applications. Teams can be managed via the API or auto-synced from SAML groups.
# Create a team
curl -X POST http://localhost:3000/api/v1/teams \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Payments-Ops", "description": "Payment system operators"}'
# Grant team permission on an application
curl -X POST http://localhost:3000/api/v1/apps/$APP_ID/permissions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"team_id": "<team-uuid>", "level": "operate"}'API keys provide non-interactive authentication for CLI tools and scheduler integrations.
| Property | Details |
|---|---|
| Format | ac_ prefix + random string (e.g., ac_xK9m2pQ...) |
| Storage | SHA-256 hash stored in api_keys table (the raw key is never stored) |
| Scope | Same permissions as the user who created them |
| Expiry | Optional — configurable at creation time |
# Create an API key (via API)
curl -X POST http://localhost:3000/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "scheduler-prod", "expires_in_days": 365}'
# Use with CLI
export APPCONTROL_API_KEY=ac_xK9m2pQ...
appctl status $APP_ID
# Use with curl
curl -H "Authorization: Bearer ac_xK9m2pQ..." http://localhost:3000/api/v1/apps| Property | Value |
|---|---|
| Algorithm | HS256 (HMAC-SHA256) |
| Expiry | 24 hours |
| Storage | HttpOnly cookie (browser), Bearer header (API/CLI) |
| Claims | sub (user_id), org (organization_id), email, role, exp, iat, iss |
| Issuer | Configurable via JWT_ISSUER (default: appcontrol) |
OIDC implements the Authorization Code Flow. Tested with Keycloak, Okta, Azure AD, and Google Workspace.
| Variable | Required | Default | Description |
|---|---|---|---|
OIDC_DISCOVERY_URL |
Yes (to enable) | - | OpenID Connect discovery URL. Example: https://keycloak.example.com/realms/appcontrol/.well-known/openid-configuration |
OIDC_CLIENT_ID |
Yes | - | Client ID registered with the OIDC provider |
OIDC_CLIENT_SECRET |
Yes | - | Client secret |
OIDC_REDIRECT_URI |
No | /api/v1/auth/oidc/callback |
Redirect URI after authentication. Must match the provider configuration exactly. |
OIDC_SCOPES |
No | openid,profile,email |
Comma-separated OIDC scopes to request |
OIDC Flow:
User clicks "Sign in with SSO"
→ Browser redirects to /api/v1/auth/oidc/login
→ Backend redirects to OIDC provider (authorization_endpoint)
→ User authenticates at the OIDC provider
→ Provider redirects back to /api/v1/auth/oidc/callback?code=...
→ Backend exchanges code for tokens (token_endpoint)
→ Backend fetches user info (userinfo_endpoint)
→ Backend creates/updates user in PostgreSQL
→ Backend returns JWT (HttpOnly cookie + JSON response)
→ User is authenticated
OIDC provider setup (Keycloak example):
- Create a realm
appcontrol - Create a client
appcontrol-frontendwith confidential access type - Set valid redirect URIs:
https://appcontrol.example.com/api/v1/auth/oidc/callback - Note the client ID and client secret
- Set environment variables on the backend:
OIDC_DISCOVERY_URL=https://keycloak.example.com/realms/appcontrol/.well-known/openid-configuration
OIDC_CLIENT_ID=appcontrol-frontend
OIDC_CLIENT_SECRET=your-client-secret
OIDC_REDIRECT_URI=https://appcontrol.example.com/api/v1/auth/oidc/callbackAuto-provisioning: Users authenticating via OIDC for the first time are automatically created with the viewer role in the default organization.
SAML implements the SP-Initiated Web Browser SSO Profile. Tested with ADFS, Azure AD, and Okta.
| Variable | Required | Default | Description |
|---|---|---|---|
SAML_IDP_SSO_URL |
Yes (to enable) | - | Identity Provider SSO endpoint URL |
SAML_IDP_CERT |
Yes | - | IdP signing certificate (PEM format, base64-encoded) |
SAML_SP_ENTITY_ID |
Yes | - | Service Provider entity ID (e.g., https://appcontrol.example.com/saml) |
SAML_SP_ACS_URL |
Yes | - | Assertion Consumer Service URL (e.g., https://appcontrol.example.com/api/v1/auth/saml/acs) |
SAML_GROUP_ATTRIBUTE |
No | memberOf |
SAML attribute name containing group memberships |
SAML_EMAIL_ATTRIBUTE |
No | email |
SAML attribute name for user email |
SAML_NAME_ATTRIBUTE |
No | displayName |
SAML attribute name for display name |
SAML_ADMIN_GROUP |
No | - | SAML group name that maps to the admin role |
SAML_WANT_ASSERTIONS_SIGNED |
No | true |
Require IdP to sign SAML assertions |
SAML Flow:
User clicks "Sign in with SSO" (configured for SAML)
→ Browser redirects to /api/v1/auth/saml/login
→ Backend generates AuthnRequest and redirects to IdP (SAML_IDP_SSO_URL)
→ User authenticates at the IdP
→ IdP POSTs SAMLResponse to /api/v1/auth/saml/acs
→ Backend validates response, extracts attributes
→ Backend syncs SAML groups → AppControl teams
→ Backend creates/updates user in PostgreSQL
→ Backend sets JWT cookie and redirects to UI
SP Metadata endpoint: GET /api/v1/auth/saml/metadata — returns XML metadata that you can import into your IdP.
SAML groups from the assertion are automatically mapped to AppControl teams via the saml_group_mappings table:
AD Group "APP_PAYMENTS_OPERATORS"
→ AppControl team "Payments-Ops"
→ Permission: operate on "Paiements-SEPA"
AD Group "APP_PAYMENTS_ADMINS"
→ AppControl team "Payments-Admin"
→ Permission: manage on "Paiements-SEPA"
AD Group "APPCONTROL_ADMINS" (= SAML_ADMIN_GROUP)
→ role=admin (org admin, implicit owner on everything)
Manage mappings via API:
# List all group mappings
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3000/api/v1/saml/group-mappings
# Create a mapping
curl -X POST http://localhost:3000/api/v1/saml/group-mappings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"saml_group": "CN=APP_PAYMENTS_OPERATORS,OU=Groups,DC=corp,DC=com",
"team_id": "<team-uuid>",
"default_role": "operator"
}'
# Delete a mapping
curl -X DELETE http://localhost:3000/api/v1/saml/group-mappings/<mapping-id> \
-H "Authorization: Bearer $TOKEN"Sync behavior on login:
- Extract group claims from the SAML assertion
- For each group, look up
saml_group_mappingsto find the target team - Add user to matched teams (if not already a member)
- Remove user from SAML-managed teams whose group is no longer in the assertion
- This ensures team membership always reflects the current AD/LDAP state
The backend is the central API server. It is configured exclusively via environment variables — no config file is needed.
| Variable | Required | Default | Description |
|---|---|---|---|
APP_ENV |
No | development |
development, staging, or production. In production, missing JWT_SECRET or DATABASE_URL causes a fatal startup error. |
PORT |
No | 3000 |
HTTP listen port |
LOG_FORMAT |
No | text |
text (human-readable) or json (structured JSON, recommended for production log aggregation) |
RUST_LOG |
No | info |
Log level filter (tracing-subscriber syntax). Examples: info, appcontrol_backend=debug, appcontrol_backend=debug,tower_http=info |
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Prod: Yes | postgresql://appcontrol:appcontrol@localhost:5432/appcontrol |
PostgreSQL 16 connection string. Must include ?sslmode=require in production. |
DB_POOL_SIZE |
No | 20 |
Maximum connections in the pool. Rule of thumb: 2-3x the number of CPU cores. |
DB_IDLE_TIMEOUT_SECS |
No | 600 |
Close idle connections after N seconds. Prevents stale connections behind load balancers with idle timeouts (e.g., AWS ALB = 350s). |
DB_CONNECT_TIMEOUT_SECS |
No | 30 |
Timeout for acquiring a connection from the pool. If the pool is exhausted, requests wait up to this duration before returning an error. |
Migrations run automatically on backend startup — no manual step required.
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET |
Prod: Yes | dev-secret-change-in-production |
JWT signing secret. Must be >= 32 characters in production. The backend will panic on startup if the secret is weak and APP_ENV=production. Generate a strong secret: openssl rand -base64 48 |
JWT_ISSUER |
No | appcontrol |
JWT iss claim value. Must match across all backend instances in a cluster. |
| Variable | Required | Default | Description |
|---|---|---|---|
HA_MODE |
No | false |
When true, rate limiting uses PostgreSQL instead of in-memory counters. Enable when running multiple backend replicas behind a load balancer. |
| Variable | Required | Default | Description |
|---|---|---|---|
RATE_LIMIT_AUTH |
No | 10 |
Authentication endpoints: max requests per IP per minute |
RATE_LIMIT_OPERATIONS |
No | 5 |
Operation endpoints (start/stop/restart): max requests per user per minute |
RATE_LIMIT_READS |
No | 200 |
Read endpoints (GET): max requests per user per minute |
| Variable | Required | Default | Description |
|---|---|---|---|
CORS_ORIGINS |
Prod: Yes | (permissive in dev) | Comma-separated allowed origins. Example: https://appcontrol.example.com,https://admin.example.com. In production, empty = reject all cross-origin requests. In development, empty = permissive. |
| Variable | Required | Default | Description |
|---|---|---|---|
RETENTION_ACTION_LOG_DAYS |
No | 0 (unlimited) |
Drop action_log entries older than N days. The action_log is append-only; this controls background cleanup only. |
RETENTION_CHECK_EVENTS_DAYS |
No | 0 (unlimited) |
Drop check_events partitions older than N days. Recommended: 90 for production (3 months of health check history). |
| Variable | Required | Default | Description |
|---|---|---|---|
SHUTDOWN_TIMEOUT_SECS |
No | 30 |
Time (seconds) to wait for in-flight requests during shutdown. Should be slightly less than Kubernetes terminationGracePeriodSeconds. |
The backend automatically adds the following security headers to every response (no configuration needed):
| Header | Value |
|---|---|
X-Frame-Options |
DENY |
X-Content-Type-Options |
nosniff |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
X-XSS-Protection |
1; mode=block |
Content-Security-Policy |
default-src 'self'; script-src 'self'; ... |
Referrer-Policy |
strict-origin-when-cross-origin |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/health |
GET | No | Health check (returns {"status":"ok"}) |
/ready |
GET | No | Readiness probe (checks DB connection) |
/metrics |
GET | No | Prometheus metrics |
/api/v1/auth/login |
POST | No | Email+password login (dev mode only) |
/api/v1/auth/dev-login |
POST | No | Email-only dev login (dev mode only) |
/api/v1/auth/oidc/login |
GET | No | OIDC login redirect |
/api/v1/auth/oidc/callback |
GET | No | OIDC callback |
/api/v1/auth/saml/login |
GET | No | SAML login redirect |
/api/v1/auth/saml/acs |
POST | No | SAML Assertion Consumer Service |
/api/v1/auth/saml/metadata |
GET | No | SAML SP metadata (XML) |
/api/v1/enroll |
POST | Token | Agent/gateway enrollment |
/api/v1/apps/** |
ALL | JWT | Application CRUD, operations |
/api/v1/teams/** |
ALL | JWT | Team management |
/api/v1/api-keys/** |
ALL | JWT | API key management |
/ws |
GET | JWT | Client WebSocket (real-time events) |
/ws/gateway |
GET | - | Gateway WebSocket (internal) |
Redis is no longer used by AppControl. Token revocation and rate limiting are handled entirely by PostgreSQL:
- Token revocation: Revoked token fingerprints are stored in the
revoked_tokenstable with automatic expiry cleanup. - Rate limiting: Counters use PostgreSQL (when
HA_MODE=true) or in-memory (single instance).
The REDIS_URL environment variable is no longer recognized.
The gateway is the network relay between the backend and agents. It is deployed close to the agents (same network zone) and maintains persistent WebSocket connections in both directions.
The gateway is configured via YAML file (/etc/appcontrol/gateway.yaml) with environment variable overrides. If no config file exists, all values come from environment variables with sensible defaults.
Config file search order:
- Path specified via
--configCLI flag /etc/appcontrol/gateway.yaml(Linux/macOS)%PROGRAMDATA%\AppControl\config\gateway.yaml(Windows)
# /etc/appcontrol/gateway.yaml
gateway:
id: "gateway-prd-01" # Unique gateway identifier (used to generate deterministic UUID)
zone: "PRD" # Network zone label (PRD, DR, DMZ, etc.)
listen_addr: "0.0.0.0" # Bind address for agent connections
listen_port: 4443 # Listen port for agent WebSocket connections
backend:
url: "ws://backend:3000/ws/gateway" # Backend WebSocket URL (MUST end with /ws/gateway)
reconnect_interval_secs: 5 # Seconds to wait before reconnecting after disconnection
tls: # Omit entire section to disable mTLS (dev only!)
enabled: true
cert_file: "/etc/appcontrol/tls/gateway.crt" # Gateway server certificate (PEM)
key_file: "/etc/appcontrol/tls/gateway.key" # Gateway private key (PEM)
ca_file: "/etc/appcontrol/tls/ca.crt" # CA certificate for verifying agent client certsEnvironment variables take precedence over YAML values.
| Variable | YAML Path | Default | Description |
|---|---|---|---|
GATEWAY_ID |
gateway.id |
gateway-01 |
Unique identifier. Used to generate a deterministic UUID v5. |
GATEWAY_SITE_ID |
gateway.site_id |
(none) | UUID of the site this gateway belongs to. Optional: gateways without a site appear as "Unassigned" in the UI. |
GATEWAY_ZONE |
gateway.zone |
(deprecated) | Deprecated. Legacy zone label. Use GATEWAY_SITE_ID instead. |
LISTEN_ADDR |
gateway.listen_addr |
0.0.0.0 |
Bind address |
LISTEN_PORT |
gateway.listen_port |
4443 |
Listen port for agent WebSocket connections |
BACKEND_URL |
backend.url |
ws://localhost:3000/ws/gateway |
Backend WebSocket URL. Must end with /ws/gateway. |
BACKEND_RECONNECT_SECS |
backend.reconnect_interval_secs |
5 |
Reconnect interval in seconds |
TLS_ENABLED |
tls.enabled |
false |
Enable mTLS (true or 1) |
TLS_CERT_FILE |
tls.cert_file |
- | Gateway server certificate path (PEM) |
TLS_KEY_FILE |
tls.key_file |
- | Gateway private key path (PEM) |
TLS_CA_FILE |
tls.ca_file |
- | CA certificate path for agent client cert verification (PEM) |
RUST_LOG |
- | appcontrol_gateway=debug |
Log level filter |
The gateway does NOT connect to PostgreSQL or any database. Its only network requirements are:
| Direction | Target | Protocol | Port |
|---|---|---|---|
| Outbound | Backend | WebSocket (ws:// or wss://) | 3000 |
| Inbound | Agents | WebSocket (ws:// or wss://) | 4443 |
This allows gateways to be deployed in isolated network zones (DMZ, remote sites, air-gapped networks) without database access.
- Auto-reconnect: If the backend connection drops, the gateway reconnects every
reconnect_interval_secsseconds. - Agent re-announce: When the backend connection is restored, the gateway re-announces all currently connected agents.
- Agent rate limiting: Built-in per-agent rate limiting to prevent a rogue agent from flooding the backend.
- Enrollment proxy: The gateway exposes
POST /enrollto proxy enrollment requests from agents that don't have mTLS certificates yet. - Health endpoint:
GET /healthreturnsok agents=N backend=connected|disconnected buffer_msgs=N buffer_bytes=N.
On Windows, the gateway can run as a Windows service:
# Install as a Windows service
appcontrol-gateway.exe service install --config C:\ProgramData\AppControl\config\gateway.yaml
# Remove the service
appcontrol-gateway.exe service uninstallOn Linux, use systemd (see Agent systemd section for a similar unit file template).
The agent runs on every monitored host. It executes health checks, start/stop commands, and reports status to the gateway.
The agent is configured via YAML file (/etc/appcontrol/agent.yaml) with environment variable overrides. If no config file exists, all values come from environment variables with sensible defaults.
Config file search order:
- Path specified via
--configCLI flag /etc/appcontrol/agent.yaml(Linux/macOS)%PROGRAMDATA%\AppControl\config\agent.yaml(Windows)
# /etc/appcontrol/agent.yaml
agent:
id: "auto" # "auto" = deterministic UUID v5 from hostname
# Or set a fixed UUID: "550e8400-e29b-41d4-a716-446655440000"
gateway:
# Simple setup — single gateway:
url: "wss://gateway.example.com:4443/ws"
# Recommended — multiple gateways with failover:
urls:
- "wss://gateway-prd-01.example.com:4443/ws"
- "wss://gateway-prd-02.example.com:4443/ws"
failover_strategy: "ordered" # "ordered" = try in list order; "round-robin" = rotate
primary_retry_secs: 300 # Attempt to return to the first (primary) gateway every 5 min
reconnect_interval_secs: 10 # Wait between reconnection attempts
tls: # Omit entire section to disable mTLS (dev only!)
enabled: true
cert_file: "/etc/appcontrol/tls/agent.crt" # Agent client certificate (PEM)
key_file: "/etc/appcontrol/tls/agent.key" # Agent private key (PEM)
ca_file: "/etc/appcontrol/tls/ca.crt" # CA for verifying the gateway server cert
labels: # Custom labels for filtering/grouping in the UI
environment: "production"
datacenter: "dc-paris-01"
os: "rhel8"
team: "platform"
log_level: "appcontrol_agent=info" # tracing-subscriber filter syntax| Variable | YAML Path | Default | Description |
|---|---|---|---|
AGENT_ID |
agent.id |
auto |
Agent ID. auto = deterministic UUID v5 from hostname. |
GATEWAY_URL |
gateway.url |
ws://localhost:4443/ws |
Single gateway URL |
GATEWAY_URLS |
gateway.urls |
- | Comma-separated list of gateway URLs for failover. Example: wss://gw1:4443/ws,wss://gw2:4443/ws |
GATEWAY_RECONNECT_SECS |
gateway.reconnect_interval_secs |
10 |
Reconnect interval in seconds |
TLS_ENABLED |
tls.enabled |
false |
Enable mTLS (true or 1) |
TLS_CERT_FILE |
tls.cert_file |
- | Client certificate path (PEM) |
TLS_KEY_FILE |
tls.key_file |
- | Private key path (PEM) |
TLS_CA_FILE |
tls.ca_file |
- | CA certificate path (PEM) |
RUST_LOG |
- | appcontrol_agent=debug |
Log level filter |
The agent only makes outbound connections:
| Direction | Target | Protocol | Port |
|---|---|---|---|
| Outbound | Gateway | WebSocket (wss://) | 4443 |
- No inbound ports needed — firewall-friendly.
- No direct connection to backend or database.
- Works behind NAT, corporate firewalls, and proxies.
| Path (Linux) | Path (Windows) | Purpose |
|---|---|---|
/etc/appcontrol/agent.yaml |
%PROGRAMDATA%\AppControl\config\agent.yaml |
Configuration file |
/etc/appcontrol/tls/ |
%PROGRAMDATA%\AppControl\config\tls\ |
TLS certificates |
/var/lib/appcontrol/buffer-{agent-id} |
%PROGRAMDATA%\AppControl\buffer-{agent-id} |
Offline message buffer (sled embedded DB) |
Critical design rule: Processes started by the agent MUST survive agent crash or restart. The agent uses double-fork + setsid (Unix) to ensure:
- The child process is reparented to init/systemd (PID 1)
- The child has its own session (setsid)
- Agent crash, restart, or upgrade does not kill managed processes
When the gateway connection is lost, the agent buffers messages in a local sled embedded database. When connectivity is restored, buffered messages are replayed in order. The buffer path is /var/lib/appcontrol/buffer-{agent-id}.
When multiple gateway URLs are configured:
orderedstrategy: Try gateways in list order. First available wins. Periodically retry the primary (first in list) everyprimary_retry_secsto return to the preferred gateway when it recovers.round-robinstrategy: Rotate through gateways on each reconnection attempt.
# /etc/systemd/system/appcontrol-agent.service
[Unit]
Description=AppControl Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/appcontrol-agent --config /etc/appcontrol/agent.yaml
Restart=always
RestartSec=10
User=appcontrol
Group=appcontrol
# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/appcontrol
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now appcontrol-agentOn Windows, the agent can run as a Windows service:
appcontrol-agent.exe service install --config C:\ProgramData\AppControl\config\agent.yaml
appcontrol-agent.exe service uninstallThe frontend is a React SPA served by nginx. Configuration is minimal.
The frontend container bundles nginx which serves the static React build and proxies API/WebSocket requests to the backend.
| URL Pattern | Proxied To | Purpose |
|---|---|---|
/api/** |
http://backend:3000/api/** |
REST API |
/ws |
http://backend:3000/ws |
Client WebSocket |
/** |
Static files (SPA fallback to index.html) |
React SPA |
Nginx adds the same security headers as the backend (defense in depth):
X-Frame-Options: DENYX-Content-Type-Options: nosniffStrict-Transport-Security: max-age=31536000; includeSubDomainsContent-Security-Policy: default-src 'self'; ...
Static assets (.js, .css, .png, .woff2, etc.) are cached for 1 year with Cache-Control: public, immutable. Vite generates hashed filenames, so cache busting is automatic on each build.
The CLI is configured via environment variables and command-line flags.
| Variable | CLI Flag | Default | Description |
|---|---|---|---|
APPCONTROL_URL |
--url |
http://localhost:3000 |
Backend API URL |
APPCONTROL_API_KEY |
--api-key |
- | API key for authentication (format: ac_...) |
Designed for scheduler integration (Control-M, AutoSys, Dollar Universe, TWS):
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Operation failed |
2 |
Timeout |
3 |
Authentication error |
4 |
Resource not found |
5 |
Permission denied |
# Configure
export APPCONTROL_URL=https://appcontrol.example.com
export APPCONTROL_API_KEY=ac_xK9m2pQ...
# List applications
appctl list
# Start an application (waits for completion, 2min timeout)
appctl start $APP_ID --wait --timeout 120
# Check status
appctl status $APP_ID
# Stop application (reverse DAG order)
appctl stop $APP_ID --wait
# Restart failed branch only
appctl start-branch $APP_ID --component $COMPONENT_ID --wait
# Run diagnostics
appctl diagnose $APP_ID --level 1 # Health
appctl diagnose $APP_ID --level 2 # Integrity
appctl diagnose $APP_ID --level 3 # InfrastructureAppControl CA (self-signed or enterprise PKI)
├── Gateway server certificate
│ CN=appcontrol-gateway
│ SAN: DNS:gateway.example.com, DNS:*.gateway.internal
└── Agent client certificates (one per agent)
CN=agent-{hostname}
Use your organization's existing PKI. Provide the CA, gateway cert, and agent certs.
# Gateway config
tls:
enabled: true
cert_file: "/path/to/enterprise-gateway.crt"
key_file: "/path/to/enterprise-gateway.key"
ca_file: "/path/to/enterprise-ca.crt" # Your corporate CA
# Agent config
tls:
enabled: true
cert_file: "/path/to/enterprise-agent.crt"
key_file: "/path/to/enterprise-agent.key"
ca_file: "/path/to/enterprise-ca.crt" # Same corporate CAAppControl can auto-generate a CA per organization on first startup. Agents then enroll via the enrollment API to obtain their certificates. This eliminates manual certificate management.
The backend auto-initializes PKI (CA) for organizations that don't have one. Agents call POST /api/v1/enroll (proxied through the gateway) to obtain their client certificates.
Automate certificate issuance and renewal in Kubernetes. See PRODUCTION_DEPLOYMENT.md for full cert-manager setup.
Generate certificates manually with OpenSSL:
# 1. Create CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 -subj "/CN=AppControl CA"
# 2. Generate gateway certificate
openssl genrsa -out gateway.key 2048
openssl req -new -key gateway.key -out gateway.csr -subj "/CN=appcontrol-gateway"
openssl x509 -req -in gateway.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out gateway.crt -days 365
# 3. Generate agent certificate (repeat per agent)
openssl genrsa -out agent-host01.key 2048
openssl req -new -key agent-host01.key -out agent-host01.csr -subj "/CN=agent-host01"
openssl x509 -req -in agent-host01.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out agent-host01.crt -days 365Agent connects to Gateway:
1. Gateway presents its server cert → Agent verifies against ca_file ✓
2. Agent presents its client cert → Gateway verifies against ca_file ✓ (mTLS)
3. Gateway computes SHA-256 fingerprint of agent cert for audit logging
4. Connection accepted → WebSocket upgrade → Agent registers with ID + hostname
# Start with latest release
docker compose -f docker/docker-compose.release.yaml up -d
# Start with a specific version
APPCONTROL_VERSION=0.2.0 docker compose -f docker/docker-compose.release.yaml up -dServices and ports:
| Service | Image | Port | Description |
|---|---|---|---|
postgres |
postgres:16-alpine |
5432 | PostgreSQL database |
backend |
ghcr.io/xcomponent/appcontrol-release-backend |
3000 | API server |
frontend |
ghcr.io/xcomponent/appcontrol-release-frontend |
8080 | React SPA (nginx) |
gateway |
ghcr.io/xcomponent/appcontrol-release-gateway |
4443 | Agent relay |
Default environment variables in docker-compose.release.yaml:
postgres:
POSTGRES_DB: appcontrol
POSTGRES_USER: appcontrol
POSTGRES_PASSWORD: appcontrol_dev
backend:
DATABASE_URL: postgres://appcontrol:appcontrol_dev@postgres:5432/appcontrol
PORT: "3000"
JWT_SECRET: dev-secret-change-in-production # CHANGE for production!
RUST_LOG: info,appcontrol_backend=debug
gateway:
LISTEN_ADDR: "0.0.0.0"
LISTEN_PORT: "4443"
BACKEND_URL: ws://backend:3000/ws/gateway
RUST_LOG: info,appcontrol_gateway=debug# Infrastructure only (PostgreSQL)
docker compose -f docker/docker-compose.dev.yaml up -d
# Full stack (builds from Dockerfiles)
docker compose -f docker/docker-compose.yaml up -d --build-
APP_ENV=production -
JWT_SECRETset to a strong random value (>= 32 chars):openssl rand -base64 48 -
DATABASE_URLpoints to managed PostgreSQL 16 with?sslmode=require -
CORS_ORIGINSset to your frontend URL(s) -
LOG_FORMAT=jsonfor log aggregation (ELK, Datadog, Loki) -
DB_POOL_SIZEtuned (default 20 is fine for most deployments) -
HA_MODE=trueif running multiple backend replicas -
RETENTION_CHECK_EVENTS_DAYS=90(or appropriate retention for your compliance needs) - OIDC or SAML configured for SSO
-
SHUTDOWN_TIMEOUT_SECS< KubernetesterminationGracePeriodSeconds
-
tls.enabled=truewith valid certificates -
gateway.zoneset correctly (PRD, DR, DMZ, etc.) -
backend.urlpoints to the correct backend WebSocket endpoint (ends with/ws/gateway) - Deployed in the same network zone as the agents it serves
- Health check configured:
GET /health
-
tls.enabled=truewith valid client certificate -
gateway.urlsset with failover targets (at least 2 gateways recommended) - Labels configured for inventory and filtering
-
/var/lib/appcontrol/directory exists and is writable (for offline buffer) - systemd unit file (Linux) or Windows service installed for auto-restart
-
failover_strategyset (orderedfor active/standby,round-robinfor load distribution)
- PostgreSQL 16 (no other database supported)
- SSL enabled (
sslmode=requirein connection string) - Regular backups configured
- Connection pooling (PgBouncer) if > 100 agents
- All traffic encrypted (mTLS for agent↔gateway, TLS for everything else)
- JWT secret rotated periodically
- API keys have expiry dates set
- SAML assertions are signed (
SAML_WANT_ASSERTIONS_SIGNED=true) - CORS origins restricted to your frontend domain only
- No dev endpoints accessible (
APP_ENV=productiondisables/auth/loginand/auth/dev-login)