# Securing the instance-state JSON on nginx

The portal's `config.json` (and the router's `router-config.json`) live **inside the
webroot** as shipped, so they may be fetchable by URL. `config.json` holds the bcrypt
admin-credential hash, so on a public host you want direct HTTP access to it blocked.

**Apache / LiteSpeed users need do nothing** — the bundle ships a `.htaccess` in `int/`
and `ext/` that already denies `*.json`. (Confirm it took effect: if Apache runs with
`AllowOverride None`, `.htaccess` is silently ignored.)

**nginx ignores `.htaccess`.** Add the equivalent to your `server` (or `location`) config:

```nginx
# Inside the server/location block that serves the LMS paths.
# Broad form -- deny every .json under the LMS root:
location ~ \.json$ {
    deny all;
    return 404;
}
```

`return 404` (rather than `403`) hides the file's existence. Reload and verify:

```sh
nginx -t && systemctl reload nginx
curl -i https://your-host/lms/int/config.json   # expect 404/403, NOT 200 + JSON
```

> Why this is safe: the portal and router read these files **server-side** (PHP
> `file_get_contents`), never via a client-side fetch, so denying HTTP access does not
> break either page. The only client-side request the router makes is for the canary
> **image** (a PNG), which is unaffected.

## Stronger isolation (optional, per-deployment)

The `.htaccess` / nginx `deny` rule keeps the file off the public web, but it still sits
in the webroot. For stronger isolation, move `config.json` / `router-config.json`
**outside the webroot** entirely and adapt the two PHP files to read them from that path
(e.g. via an `APP_*` environment variable). That is a hardening step **you** opt into for
your environment — the stock install does not do it for you. With the deny rule above in
place, the credential hash is already off the public web, so this is belt-and-suspenders,
not a prerequisite.
