Skip to content

Security Model

This appliance sits in the path of your users' web traffic and holds a record of where they went. That makes it worth attacking, and the design assumes so.

This page describes what protects what, and — as importantly — what it does not protect against.

Two processes, deliberately

┌────────────────────────┐        ┌──────────────────────────┐
│ cloudinfra-proxyd      │        │ cloudinfra-privhelper    │
│ runs as: cloudinfra    │◄──────►│ runs as: root            │
│ no capabilities at all │  Unix  │ closed set of operations │
│ parses logs, serves UI │ socket │ nothing else             │
└────────────────────────┘        └──────────────────────────┘

The process with the attack surface — the one parsing proxy logs and handling administrator input over HTTP — holds no privileges whatsoever. It cannot write /etc/squid, cannot restart services, cannot read the cache directory.

Anything privileged goes to a second, much smaller process over a local socket.

The closed verb set

The console asks for one of a fixed list of named operations. It never passes a file path, a command fragment or a configuration body across that boundary.

Operation Does
ping Is the helper alive
service.status Read Squid's unit state
service.reload squid -k reconfigure
service.restart Restart Squid
squid.version Read the engine version
config.validate Parse live or candidate
config.install Move the staged tree into place
config.snapshot Copy the live tree aside
config.restore Put a snapshot back
system.facts Read-only maintenance facts

Ten operations. Every path they touch is compiled in. Every argument is a typed value — an enum, or an integer that is range-checked — and the helper re-validates everything even though the caller is our own process.

The usual shortcut here is a sudoers entry with a wildcard. That is how appliances get compromised, because any argument reaching a shell is an injection point. Nothing here reaches a shell: commands are executed as an argument array with a minimal environment.

A test enforces the size of that list. Adding an operation fails the build unless somebody also writes down why it has to run as root.

Confinement

The console's systemd unit removes what it does not need: no capabilities, a read-only filesystem except three specific paths, private /tmp, no new privileges, a system-call filter, and hard limits of 512 MB memory and 50% CPU so a runaway query is killed rather than competing with the proxy.

Authentication

Passwords Argon2id, 64 MiB per verification
Session tokens 256-bit random; only a SHA-256 hash is stored
Cookies __Host- prefixed, HttpOnly, SameSite=Strict, HTTPS only
CSRF Double-submit token required on every state-changing request
Rate limiting Per address, plus per-account lockout
Sessions 8 hours absolute, 60 minutes idle

Only the hash of each session token is stored, so a read of the database — by a backup, a support bundle or a leak — hands nobody a usable session. The console never displays a token or its hash.

Concurrent password verifications are bounded, because each allocates 64 MiB and the service is capped at 512 MB. Unbounded concurrency there is an out-of-memory condition waiting to be triggered.

Secrets

Secret Where it lives Protection
Administrator password cloudinfra.db Argon2id hash
Session tokens cloudinfra.db SHA-256 hash only
TLS private key /etc/cloudinfra/tls/key.pem Generated per instance, root-owned
Directory bind password /etc/squid/auth-bind.secret 0640 root:squid

The directory bind password is the interesting one, because it must be readable by a program Squid spawns. It is deliberately not in squid.conf, which is world-readable and is displayed in the console's configuration preview, and deliberately not a command-line argument, which would appear in the process list to any local user running ps.

Nothing is shared between instances

The image is sealed with no credentials in it. First boot generates:

  • A TLS certificate and key, unique to that instance
  • An administrator password, random per instance
  • The client network, detected from cloud metadata

A pre-seal check fails the build if a certificate, database or credential file is found in the image. Two instances launched from the same image share nothing.

The initial password is written to the instance console output so you can retrieve it by proving control of the instance rather than by SSH — and the console forces you to change it at first sign-in, because that output is a bootstrap token rather than a credential.

Untrusted input

Three things arrive from outside and become configuration or display:

Proxy logs. Written by Squid from data attackers control — URLs, user agents. Free-text fields are percent-encoded by Squid, so a tab or newline cannot forge or split a record. The parser rejects malformed lines rather than guessing.

The Microsoft 365 feed. Somebody else's JSON over the network becoming proxy rules. Every hostname and range is validated; anything that would not make a valid ACL entry is dropped and counted, so a change in the feed's shape shows up rather than quietly shrinking your lists.

Administrator input. Rules, lists and directory settings become Squid directives. Values that could break out of the directive they are rendered into — quotes, newlines, LDAP metacharacters — are refused rather than escaped. Nothing legitimate contains them, and refusing stays correct as the rendering changes around it.

The audit trail

Every administrative action is recorded with who, when, what, the result and the source address. Entries are written before an action is attempted and updated with the outcome, so an action that crashes the process still leaves a trace.

Reads are audited as well as writes — log searches and analytics exports — because the trail exists to answer who looked at what, not only who changed what, and proxy logs contain your users' browsing history.

Supply chain

The console is a single static Go binary with three third-party libraries:

Library For
golang.org/x/crypto Argon2id
golang.org/x/net Public suffix list
modernc.org/sqlite Pure-Go SQLite

Everything else is the standard library. There is no JavaScript framework, no charting library, no icon font and no web assets fetched at runtime — the console is served entirely from the binary.

This is a deliberate cost. A security appliance's own dependency tree is part of its attack surface, and it is what a marketplace vulnerability scan reports against.

Notably, directory authentication added no dependency: Squid's own LDAP helpers do that work.

What this does not protect against

Stated plainly, because a security page that only lists strengths is marketing.

It does not decrypt HTTPS. No TLS interception, so no visibility into paths or content, and no scanning of encrypted traffic. That is a deliberate position — interception means installing a CA on every client and holding a key that can impersonate any site.

It is not a malware scanner or a web categorisation service. It blocks what you tell it to block.

Anyone who can reach port 8443 can attempt to sign in. The console defends itself, but the security group is your first control and should be tight.

Root on the instance is game over. Anyone with root can read the databases and the logs. Standard for an appliance, worth stating.

A backup contains your full access policy. No credentials, so it is safe to store in version control — but it is not public information either. Use a passphrase if it leaves your control.