←Research
Researchvulnerability9 min read

AReaL PR #1323 refuses the default admin API key on network-facing proxy binds

AReaL's proxy rollout server used a public default admin API key while binding to a network interface by default. PR #1323 turns that insecure default into a startup failure.

AReaL PR #1323 refuses the default admin API key on network-facing proxy binds

AReaL's experimental OpenAI proxy rollout server accepted a publicly documented default admin API key while listening on a network-facing interface by default. The key, areal-admin-key, was the sole barrier in front of administrative routes such as grant_capacity, start_session and export_trajectories. Anyone who could reach the proxy port and read the source could authenticate.

I identified the issue and submitted PR #1323, which was applied on 11 May 2026. The fix refuses to start the proxy rollout server with the default admin key when the configured bind address is not loopback. Local development still works. Operators can still opt in explicitly. The unsafe default no longer starts silently.

This is CWE-798, Use of Hard-coded Credentials. It also overlaps with CWE-1188, Insecure Default Initialization of Resource, because the deployment starts in an insecure state unless the operator notices and changes a default secret before exposure.

What made the default exploitable

The vulnerable path sits in areal/experimental/openai/proxy/proxy_rollout_server.py. Before PR #1323, _setup_openai_client() assigned the configured admin key to the module global and emitted only a warning if the value was still the default:

with _lock:
    _admin_api_key = agent_cfg.admin_api_key
    if _admin_api_key == DEFAULT_ADMIN_API_KEY:
        logger.warning(
            "Using default admin API key. Change 'admin_api_key' in "
            "AgentConfig for non-local deployments."
        )

A warning is not a security boundary. The server still starts. Detached training runs, containers and remote jobs often bury startup logs where the operator will not see them until after the service is already reachable.

The second half of the issue was the bind behaviour. The CLI default host was 0.0.0.0. In the startup path described in the PR, that value resolves to the host's external IP through gethostip(). The uvicorn.run() call also hardcoded host="0.0.0.0", so the server listened on all interfaces rather than loopback. The default deployment therefore combined a network-facing listener with a public administrative credential.

The admin check itself was direct. Routes used Depends(_require_admin), and _require_admin compared the presented token to _admin_api_key with hmac.compare_digest(token, _admin_api_key). Constant-time comparison is the right primitive for comparing secrets, but it does not help when the secret is published and reused across deployments. In this case, credential discovery was not part of the attack. The repository already disclosed the value.

The exploit preconditions were narrow but realistic:

  1. The proxy port is reachable from the attacker's network position.
  2. OpenAIProxyConfig.admin_api_key is still the default.

For an unmodified invocation, both conditions held. That matters because the protected routes are not cosmetic. Capacity grants, session creation and trajectory export are control-plane actions. They let an authenticated caller influence rollout behaviour and retrieve data that should not be available to an arbitrary network peer.

The fix in PR #1323

The fix changes _setup_openai_client() from warning-only behaviour to fail-closed validation. It first stores the requested key in a local variable:

requested_admin_key = agent_cfg.admin_api_key

It then checks whether that key still equals DEFAULT_ADMIN_API_KEY. If it does, the code permits only two cases:

loopback_hosts = {"127.0.0.1", "::1", "localhost"}
allow_override = (
    os.environ.get("AREAL_ALLOW_DEFAULT_ADMIN_KEY", "0") == "1"
)
if _server_host in loopback_hosts or allow_override:
    logger.warning(...)
else:
    raise RuntimeError(...)

The failure message names the unsafe host, the default key and the exact remediation: set admin_api_key in OpenAIProxyConfig, or set AREAL_ALLOW_DEFAULT_ADMIN_KEY=1 to acknowledge the risk in a trusted environment.

That explicit override is important. Some research deployments run inside a private cluster or a disposable test environment where the operator may intentionally accept the risk. Security fixes that break every tutorial and every local workflow tend to get reverted or bypassed badly. This one keeps loopback development unchanged and makes risky network exposure a conscious act rather than an accidental default.

The PR also changes the uvicorn.run() call:

uvicorn.run(
    app,
    host=_server_host,
    port=_server_port,
    ...
)

Previously, the bind address was hardcoded to 0.0.0.0. After the fix, the listener binds to the same host value that the validation logic evaluated. That alignment is the difference between a policy and a comment.

The review issues that mattered

The final diff is small, but the review caught two issues that are worth spelling out because they are common in security patches.

The first issue was assignment before validation. An earlier version wrote the default key into the global _admin_api_key and then raised RuntimeError when the host was non-loopback. If that exception occurred inside a handler while the server kept running, the global could already contain the public default. The code would have failed loudly while still leaving the dangerous credential active.

The final version validates requested_admin_key first and only commits it to the global after the check passes:

# Only commit the key to the global after validation has passed.
with _lock:
    _admin_api_key = requested_admin_key

That order matters. Security validation should happen before state mutation when the mutated state is itself security-sensitive.

The second issue was host validation without host binding. If the code had validated _server_host but continued to call uvicorn.run(..., host="0.0.0.0", ...), then --host 127.0.0.1 would pass the loopback check while the actual listener still accepted external connections. The final patch removes that mismatch.

These are not theoretical niceties. Both would have left a bypass in a patch whose purpose was to make network exposure safe. Small authentication fixes often fail at the boundary between configuration, global state and server startup because each piece is locally reasonable. The vulnerability sits in the composition.

Why this belongs in the AI security file

AReaL is not a home router or an abandoned admin panel. It is AI infrastructure. The proxy rollout server exists to mediate OpenAI-compatible rollout workflows, manage sessions and export trajectories. That makes its admin API part of the control plane around model training or agent evaluation work.

The bug class is old. The context is new. AI systems increasingly ship with local proxies, orchestration daemons, tool routers, MCP hubs and evaluation runners. Many begin as developer conveniences bound to localhost. Then they are containerised, deployed to shared machines, exposed through notebooks or run inside clusters where 0.0.0.0 is treated as normal. A static admin key that felt harmless in a local prototype becomes a control-plane credential on a network service.

This blog has covered the same pattern from several angles. MCPHub shipped every fresh installation with the same admin password, then fixed it by generating a unique credential or accepting an operator-supplied password. gptme passed API keys on the command line, exposing secrets through the operating system process table. Non-human identity sprawl is the broader version of the same problem: automated systems now hold powerful credentials that are rarely governed with the same discipline as human accounts.

AReaL's issue is a clean example because there is no clever exploit chain. No prompt injection. No model jailbreak. No speculative architecture diagram with arrows and a red skull. A network service used a known admin key and listened beyond loopback. The fix is correspondingly plain: do not start in that state.

That is often what AI security looks like in practice. The system may be novel, but the failure mode is a known web and infrastructure bug carried into a new control plane.

Prior art in hardcoded administrative credentials

MITRE's CWE-798 describes the core issue: software that contains hardcoded credentials can allow attackers to authenticate without needing to compromise a user's secret. CWE-1188 covers insecure default initialisation, where a product starts with unsafe permissions or credentials unless the user changes them.

The history is not short. CVE-2020-29583 affected Zyxel USG firmware version 4.60. NVD describes an undocumented zyfwp account with an unchangeable password present in cleartext in firmware, usable for SSH or web interface login with admin privileges. Its CVSS v3.1 base score is 9.8.

A more recent example, CVE-2025-34223, affected Vasion Print deployments before the fixed versions. NVD describes a default admin account and installation-time endpoint that could let an unauthenticated remote attacker obtain full administrative control during setup. Its CVSS v3.1 base score is also 9.8, with a CVSS v4.0 score of 10.0.

Those products are very different from AReaL, but the security lesson is the same. A documented default credential is not authentication. It is a setup mechanism that must be constrained to local bootstrap, replaced with a unique secret or refused before the service becomes reachable.

OWASP's A07:2021 Identification and Authentication Failures groups this family of mistakes under broken authentication practice. The most useful reading is blunt: do not ship or deploy with default credentials, particularly for administrative interfaces.

What remains after the fix

PR #1323 removes the unsafe default state for the proxy rollout server. Existing callers that set a custom admin_api_key are unaffected. Loopback development still produces a warning. Non-loopback deployment with the public default now fails before the key is committed to the global and before the validated host can diverge from the actual bind.

The remaining risk is the explicit override. AREAL_ALLOW_DEFAULT_ADMIN_KEY=1 can still put the public key on a reachable service. That is intentional, documented risk acceptance rather than an accidental configuration. It should be rare, short-lived and limited to environments where network reachability is already controlled.

The better long-term pattern for AI infrastructure is to make administrative secrets mandatory at deploy time or generate them per installation. Refusing known defaults on external binds is the minimum safe line. It is not a substitute for secret rotation, scoped tokens or stronger deployment guidance, but it closes the path where an operator gets compromised simply by accepting every default the project offered.

The uncomfortable part is how much of the AI stack still treats admin endpoints as local development plumbing. The moment that plumbing binds to a real interface, it becomes infrastructure. Infrastructure does not get to rely on a warning in a log file that nobody reads.

Newsletter

One email a week. Security research, engineering deep-dives and AI security insights - written for practitioners. No noise.