<a id="explanation-security"></a>

# Security

The charm makes deliberate security decisions across three areas: client to charm,
charm to backend, and internal process security.

## Client to charm

### Transport security

The charm terminates TLS for incoming client requests when the `certificates` relation
(interface: `tls-certificates`) is established with a TLS certificate provider such as
`lego` (referred to as `cache-lego`).

When a certificate is available, nginx listens on the allocated port with SSL enabled
(`listen <port> ssl`) using the PEM stored at `/etc/nginx/certs/content-cache-charm.pem`
that contains the certificate and key.
The `cache-backend` relation data returns `https://` URLs so that HAProxy can connect
over HTTPS. HAProxy integrates with `cache-lego` via `certificate_transfer` to obtain
the CA certificate needed to trust the content-cache certificate.

If the `certificates` relation is present but the certificate has not yet been issued,
the charm enters `WaitingStatus`. When the relation is removed, the charm deletes the
cert file and nginx reverts to HTTP.

Without the `certificates` relation, nginx listens on HTTP only. Client-facing TLS
termination must then be handled by an upstream ingress component such as `haproxy`
configured with the `ingress-configurator` charm.

### Client authentication

The charm provides no client authentication mechanism. Any client that can reach the charm’s HTTP listener can request cached content. This is an intentional design constraint: the charm is built for publicly accessible static content. There are no plans to add a native authentication feature.

### Rate limiting

The charm does not configure nginx rate limiting
([`limit_req`](https://nginx.org/en/docs/http/ngx_http_limit_req_module.html) or
[`limit_conn`](https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html)). Operators
who need protection against abuse or denial-of-service attacks must add rate limiting at a
component placed in front of the charm, such as a load balancer, reverse proxy, or Web Application Firewall (WAF).

### Client IP privacy in logs

By default, the client’s IP address is recorded as plain text in the nginx access and cache logs. The
`client-ip-hash-salt` configuration option (a user-owned Juju secret) replaces the client IP
address logged in the access and cache logs with a salted SHA-256 hash, so raw IP addresses are
never written to those two log files. This configuration does not apply to nginx’s error log, which may still
record the client address in error entries (for example, on upstream connection failures).
See [How to hash client IP addresses in logs](https://canonical.com/juju/docs/content-cache-charms/latest/how-to/hash-client-ip-addresses-in-logs/index.html.md#how-to-hash-client-ip-addresses-in-logs) for setup instructions.

Rotating the salt
changes the hash produced for a given client IP address, so logs recorded before a rotation
cannot be correlated with logs recorded afterwards. The salt is written to a single file
under nginx’s secrets directory (root-owned, `www-data`-group-readable) before nginx is told
to reload, so a newly started worker can never find it missing. Because nginx workers cache
it in memory for their lifetime, a worker still finishing in-flight requests from before a
rotation may keep using the previous salt for those requests, but the worker never falls back to logging
plaintext addresses.

## Charm to backend

### Backend protocol

Backends are specified as full URLs in the form `<http|https>://<ip>:<port>` via the
`backends` field of the `cache-config` relation data (for example, the `backends` option on
`content-cache-backends-config`, or the `backend-addresses`/`backend-ports`/`backend-protocol`
options on `ingress-configurator`). The URL scheme controls
whether nginx contacts backends over HTTP or HTTPS. Operators should use HTTPS backend URLs
unless backends do not support TLS.

### Backend SSL certificate verification

The charm contacts backends over two separate code paths: the Lua healthcheck module (periodic
health pings) and the nginx `proxy_pass` directive (actual proxied requests). These have
different SSL verification behavior.

**Healthchecks** — the `healthcheck_ssl_verify` field of the `cache-config` relation data (for
example, `healthcheck-ssl-verify` on `content-cache-backends-config`, or
`cache-healthcheck-ssl-verify` on `ingress-configurator`)
controls whether the Lua healthcheck module verifies the backend SSL certificate during health
pings. The configuration defaults to `true`. Setting the configuration to `false` disables certificate verification for
healthchecks and should only be used in controlled environments, for example, when backends
use self-signed certificates on a trusted private network.

**Proxied requests** — when the `receive-ca-cert` relation provides a CA certificate, the charm
configures nginx with
[`proxy_ssl_verify`](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ssl_verify) enabled
and `proxy_ssl_trusted_certificate` pointing to `/etc/nginx/certs/ca-bundle.pem`. This bundle
contains the system CA store (`/etc/ssl/certs/ca-certificates.crt`) merged with every CA
certificate received over `receive-ca-cert`, so nginx trusts both publicly-signed and
relation-provided backend certificates. This is distinct from
`/etc/nginx/certs/content-cache-charm.pem`, which holds only the charm’s own client-facing
leaf certificate, chain, and private key. It contains no CA
material. If HTTPS backends are configured but no `receive-ca-cert` relation is present, the
charm enters `WaitingStatus` and nginx is not reconfigured. Traffic is paused until a CA
certificate is supplied.

## Internal

### Process user

On Ubuntu, the nginx package is configured to run worker processes as `www-data` via the
[`user` directive](https://nginx.org/en/docs/ngx_core_module.html#user) in the system
`nginx.conf`. `www-data` is a low-privilege system account with no login shell and no sudo
rights. If nginx were compromised, the attacker would have only the limited access of
`www-data` — they cannot read arbitrary files owned by other users or escalate privileges
without a separate exploit.

The charm creates and owns all cache files as `www-data`. The cache directory
at `/data/nginx/cache/` is owned by `www-data` (set via `os.chown` in the charm code).
CA certificate files received via `receive-ca-cert` are stored at `/etc/nginx/certs/`
and are readable by nginx at runtime.

### Status page access

The nginx status page at `/nginx_status` and the backend health status page at
`/nginx_backends_status` are served on a dedicated port (`127.0.0.1:30200`) that is separate from
the ports used for cache traffic, and are further restricted to `127.0.0.1` using the nginx
[`allow`/`deny` directives](https://nginx.org/en/docs/http/ngx_http_access_module.html).
External clients cannot access these endpoints. Serving them on their own port (rather than the
standard HTTP/HTTPS ports) also avoids a `bind()` conflict when content-cache is co-located on the
same unit as another service that already listens on ports 80/443. This behavior is hard-coded in
the generated nginx configuration and cannot be changed via charm configuration.

### Subprocess security

The charm uses `shell=False` for all subprocess calls, preventing shell injection attacks from
malformed configuration values.

## Cached data risks

### Public-only content

The charm’s cache key is based on the nginx default `$scheme$proxy_host$request_uri`. It
does not include request headers such as `Cookie` or `Authorization`. Two requests for the
same URL with different session cookies share a single cache entry: the first response is
cached and served to every subsequent requester of that URL, regardless of their identity.

Operators must ensure that only public, non-personalized content is routed through the charm.
Routing personalized or session-dependent content through the charm means users will
receive each other’s responses.

### No cache purge

The charm provides no mechanism to purge a cached response on demand. Cache entries
are only removed when their `proxy-cache-valid` TTL expires or when nginx’s inactive
eviction removes them (default: 10 minutes of no access). If sensitive content is
accidentally cached, operators must wait for natural expiry.

## Best practices

| Practice                 | Recommendation                                                                                                                     |
|--------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| Incoming TLS             | Handle at the ingress layer (e.g. `haproxy` with `ingress-configurator`)                                                           |
| Backend protocol         | Use HTTPS backend URLs with the `receive-ca-cert` relation for verified connections                                                |
| Backend SSL verification | Integrate `receive-ca-cert` to enable `proxy_ssl_verify on` with the provided CA bundle                                            |
| Access control           | Place an authenticating reverse proxy or WAF in front if the content is not fully public                                           |
| Rate limiting            | Add rate limiting at a component placed in front of the charm (load balancer, reverse proxy, or WAF) if abuse protection is needed |
| Cached content           | Only route public, non-personalized content through the charm                                                                      |
| Machine access           | Restrict local user access to the Juju machine                                                                                     |
| Log privacy              | Configure `client-ip-hash-salt` to avoid storing client IP addresses in plain text logs                                            |
