Security~
This page covers login protection, API keys, and the audit log. For encrypting the web connection itself, see Force HTTPS in General Settings.
Cookies behind a reverse proxy (Docker)~
The Docker image relaxes a few cookie settings by default — the session cookie and the "remember me" cookie are sent without the Secure flag, and CSRF protection does not require HTTPS. This is on purpose: many Docker installs are reached directly over plain http:// on a local network (small controller devices, for example), and a Secure cookie would simply never be sent back, breaking login.
If this container is instead reached only through a reverse proxy that always terminates TLS in front of it (nginx, Nginx Proxy Manager, Cloudflare Tunnel, and similar), set the environment variable AOT_BEHIND_TLS=1 on the container. This restores the Secure flag on both cookies and makes CSRF checks require HTTPS, so a cookie can never be sent in the clear — including on the very first request a browser makes to http:// before the proxy redirects it to https://.
For the Docker distribution, set it in docker/.env (AOT_BEHIND_TLS=1) and apply it with docker compose -f docker/docker-compose.prod.yml up -d.
Only set this if every request truly arrives over HTTPS
With AOT_BEHIND_TLS=1, a browser will not send these cookies back over a plain http:// connection at all. If the reverse proxy does not actually forward every request as HTTPS (for example, it does not set the X-Forwarded-Proto header, or the container is also reachable directly on plain HTTP), login and form submissions will fail. Leave this unset for any install reached over plain HTTP, including most local/LAN Docker installs. This also means logging in directly at http://<LAN-IP>:8084, bypassing the proxy, stops working — on-site users who currently use that plain-HTTP address must switch to the HTTPS address instead once this is on.
This is independent of the Force HTTPS setting above, which controls whether AoT itself redirects http:// to https://; a reverse proxy usually already does that redirect, so AOT_BEHIND_TLS only needs to fix the cookies.
Password requirements~
A password must be at least 8 characters. Common passwords (password, qwerty123, and similar lists used in credential-stuffing attacks) are rejected even if they meet the length requirement.
AoT does not force periodic password changes. Forcing regular changes is no longer recommended practice — it tends to push people toward small predictable variations ("Password1", "Password2") rather than a genuinely different password, and is not required here.
Account lockout~
After 5 failed login attempts on an account, that account is locked for 10 minutes, regardless of which device or browser the attempts came from. The counter resets on a successful login. This applies to the password login, the keypad login, and the two-factor code (below) — a wrong code counts the same as a wrong password.
These limits are fixed and not currently exposed as a setting.
Two-factor authentication (TOTP)~
AoT supports a second login factor using the standard 6-digit, 30-second codes produced by apps such as Google Authenticator, Authy, or 1Password (RFC 6238 TOTP — the same standard those apps already implement for other services).
When enabled for a user, logging in requires the password first and then the current code from the authenticator app; neither one alone is enough.
No self-service enrollment screen yet
There is currently no page in AoT where a user can turn this on for themselves — that is planned but not yet built. Until then, enabling it requires an administrator to set it up directly (generating a secret with aot.utils.totp.generate_secret() and setting totp_secret / totp_enabled = True on the account). Once enabled, the login behavior above applies immediately with no other configuration.
API keys~
An API key authenticates requests to the HTTP API without a browser session. A single user can hold several keys at once — issue one under Manage → System Management → Users, editing the user, in the API Key section: give it a name (so you know which client it belongs to), pick its Permissions (Full access, matching the user's own permissions, or Read only, which can retrieve data but has every state-changing request rejected with 403 Forbidden), then click Generate. Issuing a new key does not affect any of that user's existing keys — each one keeps working until it is individually revoked, so separate integrations (for example ChatGPT and Claude Desktop) can run at the same time under one account, and a suspected leak can be shut off by revoking just that one key.
The key is shown only once, at the moment it is generated. AoT does not store the key itself — only a one-way hash of it — so the settings page cannot display an existing key again later. Copy it somewhere safe immediately after generating it. If it is lost, revoke it and issue a new one; there is no way to recover the old value.
The key can be presented three ways: the X-API-KEY header, HTTP Basic authentication, or (deprecated, see API.md) an api_key query parameter.
Audit log~
Manage → Audit Log (requires the "View Logs" permission — the same one used for the System Log; on the default roles that means Admin, Editor, and Monitor, not Guest or Kiosk — see Roles) is a browsable, filterable record of who did what, when, from where, and whether it succeeded.
What gets recorded~
- Login success, failure, and account lockout; logout
- User creation, modification, and deletion
- Group creation, modification, deletion, and changes to a group's membership or resource grants
- General settings changes
- Device control — a person switching an output from the dashboard, an AI agent doing so on their behalf, and an irrigation sequence opening or closing a valve. Continuous automatic control by PID/PWM controllers and the environment coordinator is deliberately not included here: it runs far too often for a row-per-action table, and is tracked instead as tags on the time-series measurement data. A driver's own startup, shutdown, or removal is recorded too, since that can change hardware state directly.
- API key generation and revocation, and use of the deprecated URL-query-string key auth
- Remote Admin token issuance
- Exporting this audit log as CSV, and requesting a software update (from the Upgrade page, or from the scheduled auto-update check)
The Action filter lists the everyday ones; a few less common actions (such as the update-request one above) are not in that dropdown but still show up when the filter is left on All.
AI tool calls and their approvals are not part of this log — they go through a separate approval trail described in Safety & Approval Model. This page only records the resulting device-control action, attributed to the AI.
Reading the table~
| Column | Meaning |
|---|---|
| Timestamp | When it happened, shown in your local time zone (stored internally in UTC) |
| Action | The event, as a domain.verb string — e.g. login.failure, output.control |
| Result | success or failure. A denied or failed attempt is recorded too, not only what went through |
| User | The account name, when the action came from a logged-in person. For device control specifically, this can instead show what triggered it — a sequence, a driver lifecycle phase (startup/shutdown), or (for the scheduled update check) auto-update |
| IP Address | The address the request came from. Blank when there was no browser request to read one from — an AI agent's command, or a driver lifecycle event |
| Target Object | What was acted on: its type and, where known, its name (an output's name, a setting's name, and so on) |
| Detail | A short free-text note. For device control this includes the channel, the requested state, and the determined origin (user, ai, sequence, lifecycle, or unknown if no code path claimed it — worth a look if you see it) |
Export CSV adds columns not shown on screen — user_id, target_id, and the before/after values as JSON, populated for actions where the changed value matters (currently settings changes).
Filtering and export~
Filter by Action, User (a partial, case-insensitive match), and Result, and choose how many rows to display (Rows, up to 1000). Export CSV downloads every row matching the current Action/User/Result filters — it ignores the Rows limit, so the file can hold more than what is shown on screen.
Retention~
Entries older than 1 year (AUDIT_LOG_RETENTION_DAYS) are removed automatically by a daily background job. This is not currently configurable from the settings page.
AI tool-call history (the MCP audit log) and its approval queue are a different kind of record from access logs, so they are removed by the same job after 90 days (MCP_AUDIT_RETENTION_DAYS).
Passwords, password hashes, and API keys themselves are never written to the audit log — only the fact that an action occurred and who performed it.
Remote Admin~
Remote Admin lets one AoT instance display data from other AoT instances. It is reached directly at /remote/setup — it is not currently linked from the main menu.
Adding a remote host asks for that host's username and password, but only to authenticate once. What is actually stored afterward is a dedicated access token issued by the remote host for this purpose — not the account's password or its hash. If that stored token were ever exposed, only Remote Admin access to that one host is affected; the account's real login credentials are not.
That one-time password is sent only over a connection verified against the remote's certificate. The certificate is fetched and pinned first, without sending any credentials; the password then travels only over a connection that this pinned certificate verifies. Earlier versions did the opposite — the password went out over an unverified connection and the certificate was taken from the reply body, so anyone in the middle could read the password and pin their own certificate for every connection that followed.
Trust on first contact still rests with you: a certificate seen for the first time cannot be checked against anything. The SHA-256 fingerprint of what was pinned is therefore shown when a host is added — compare it out of band (on the remote machine itself) before trusting the connection.
If the certificate later stops matching what was pinned, enrollment stops and nothing is sent. Should the remote genuinely have replaced its certificate, delete the remote host and add it again; deleting it also drops the old pin.
Existing remote hosts need to be re-added after upgrading
Instances upgrading from a version before this token-based scheme was introduced will find previously added remote hosts no longer connect. Removing and re-adding each one at /remote/setup re-establishes the connection with the new token.