# LAN Gateway
This page walks through one setup end to end: a small Linux box on a home or
office network, a Raspberry Pi in the case this was built for, serving HTTPS
for a name and an IP address that no public certificate authority will ever
certify.
## Why a local authority is the only option
A public authority issues a certificate only after it proves you control the
name. An ACME http-01 challenge is fetched over the internet; a dns-01
challenge is read out of public DNS. Neither one reaches `pi-stacks.local` or
`192.168.1.20`, because nothing on the internet routes to a private network.
There is no certificate to buy for these names. Serving them over HTTPS means
running an authority the machines on that network trust, which is what
[`localCa`](/config#localca) does.
## 1. Start a gateway with a local CA
```bash
rpx gateway \
--sites-dir /etc/rpx/sites.d \
--local-ca-dir /etc/rpx/local-ca \
--local-ca-hosts pi-stacks.local \
--local-ca-ips 192.168.1.20 \
--install-trust
```
On the first run this creates the Root CA and one leaf under
`/etc/rpx/local-ca`:
| File | What it is |
| --- | --- |
| `rpx-root-ca.crt` | The Root CA certificate, the file every client has to trust. |
| `rpx-root-ca.key` | The CA private key, mode `0600`. It never leaves the box. |
| `rpx-local-host.crt` | The leaf covering `pi-stacks.local` and `192.168.1.20`. |
| `rpx-local-host.key` | The leaf private key, mode `0600`. |
The leaf carries `pi-stacks.local` as a dNSName SAN and `192.168.1.20` as an
iPAddress SAN. It is served under the host's SNI name and as the listener's
default TLS context, so both `` and
`` are answered with it. The IP case needs the default
context: a browser given an IP literal sends no SNI at all.
`--install-trust` installs the CA into this box's own trust store, so `curl`
and anything else running locally accepts the certificate. It needs root. A
trust store that cannot be written is a warning, not a startup failure.
## 2. Give the gateway something to route
A gateway with no fragments answers 404 to everything. Each app owns one file
under the sites directory, named for its slug:
```json
// /etc/rpx/sites.d/dashboard.json
{
"slug": "dashboard",
"proxies": [
{ "from": "localhost:3000", "to": "pi-stacks.local" }
]
}
```
Deploys replace their own file and nothing else. Fragments are merged in
filename order, duplicate routes are resolved first writer wins, and a
fragment that fails to parse is logged and skipped so one bad file cannot take
the other apps down. The full rules are in
[gateway mode](/config#merge-rules).
The gateway never touches `/etc/hosts`. On a box with real DNS, or with the
name published over mDNS, that file is not the routing table and rpx leaves it
alone.
## 3. Trust the CA on the other devices
Every laptop and phone that opens the site has to trust
`/etc/rpx/local-ca/rpx-root-ca.crt`, and each platform wants it in a different
container. rpx produces the CA; [tlsx](https://github.com/stacksjs/tlsx) is
what mints and trusts it, and its `export-ca` and `trust-instructions`
commands cover exporting the file and the per-platform steps, including the
extra step iOS needs after installing a profile.
## 4. Run it under systemd
The `rpx` CLI entry point currently hangs at startup on Linux
([stacksjs/rpx#2267](https://github.com/stacksjs/rpx/issues/2267)), so run the
gateway from a launcher that calls `startGateway` instead. The gateway
function itself is unaffected.
```ts
// /etc/rpx/gateway.ts
import { startGateway } from '@stacksjs/rpx'
await startGateway({
sitesDir: '/etc/rpx/sites.d',
localCa: {
dir: '/etc/rpx/local-ca',
hosts: ['pi-stacks.local'],
ips: ['192.168.1.20'],
installTrust: true,
},
maxTlsContexts: 64,
})
```
Compile it once, then point a unit at the binary so the hot path does no
TypeScript parsing:
```bash
bun build --production --compile --outfile /etc/rpx/gateway /etc/rpx/gateway.ts
```
```ini
# /etc/systemd/system/rpx.service
[Unit]
Description=rpx gateway
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/etc/rpx/gateway
Environment=RPX_WORKERS=1
Environment=RPX_REUSE_PORT=0
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
It runs as root because it binds `:80` and `:443`.
## 5. Read the startup line
The gateway writes one line to stderr on every start, which `journalctl -u
rpx` captures:
```
[rpx gateway] 3 route(s) from /etc/rpx/sites.d; listening on :443 (https)
```
Read it before anything else. A gateway that came up holding no routes and a
gateway that never came up at all look the same from a browser, and this line
separates them. `[rpx gateway] failed to start:` means the listeners never
bound and carries the reason. The other lines worth knowing are listed under
[gateway mode](/config#what-it-prints-on-startup).
Then check the certificate from the box itself:
```bash
curl --cacert /etc/rpx/local-ca/rpx-root-ca.crt https://pi-stacks.local/
curl --cacert /etc/rpx/local-ca/rpx-root-ca.crt https://192.168.1.20/
```
Both should answer without `--insecure`. If the second one fails while the
first succeeds, the address is missing from `--local-ca-ips`.
## 6. Keep the memory bounded
A Pi shares its 4 to 8 GB with every app the gateway fronts, so run one
process and cap the certificate set:
```bash
RPX_WORKERS=1 RPX_REUSE_PORT=0
```
with `maxTlsContexts` (the `--max-tls-contexts` flag) at whatever covers the
hosts you actually route. Every SNI entry keeps a parsed certificate and key
alive in OpenSSL for the life of the listener, and a certificate directory
that has accumulated retired sites and mail certificates is memory the board
cannot spare. When the set exceeds the cap, the first N entries are kept, a
local CA leaf among them, and one warning names every host that was dropped.
See the [low-memory setting](/advanced/configuration#low-memory-setting).
## Renewal
The leaf is valid for 825 days by default and is re-minted on start when fewer
than 30 days remain (`validityDays` and `renewBeforeDays`). It is also
re-minted when a host or IP is added to the configuration, when its key stops
matching, or when the CA that signed it has been replaced. Restarting the
gateway is the whole renewal procedure; the Root CA itself stays put, so
already-trusting devices stay trusting.
## Serving plain HTTP instead
Behind something else that already terminates TLS, `--no-https` binds a single
plain HTTP listener on the HTTP port and nothing at all on the HTTPS port, no
redirect and no certificate work, even when a local CA is configured:
```bash
rpx gateway --sites-dir /etc/rpx/sites.d --no-https --http-port 8080
```
## Related
Stacks drives this whole sequence for a Raspberry Pi through its
`buddy server:*` commands, which flash the card, adopt the host and deploy the
fragments this gateway reads.