# Security

> Harden a Compose installation: open ports, firewall, secrets, TLS, admin accounts and updates.

Source: https://docs.grovs.io/docs/self-hosting/operate/security

The stack ships with private databases, generated secrets and automatic HTTPS. The steps below close what is left to you as the server owner.

## Exposed ports

| Port  | Opened by   | Who needs it                                                               |
| ----- | ----------- | -------------------------------------------------------------------------- |
| `80`  | `proxy`     | Everyone. Caddy uses it for certificate validation and redirects to HTTPS. |
| `443` | `proxy`     | Everyone: dashboard, API, SDKs and links                                   |
| `22`  | Your server | Only you, for SSH                                                          |

PostgreSQL, Redis, ClickHouse, `web` and `dashboard` publish no ports. They talk over the private Compose network, and only the proxy reaches `web` and `dashboard`. Keep it that way: do not add `ports:` entries for the databases.

The local trial override publishes `web` and `dashboard` directly on `GROVS_WEB_PORT` and `GROVS_DASHBOARD_PORT`. Never use it on a public server.

## Firewall

Use your provider's firewall in front of the server:

| Direction | Rule                             |
| --------- | -------------------------------- |
| Inbound   | TCP 80 and 443 from the internet |
| Inbound   | TCP 22 from your own IP only     |
| Outbound  | DNS, HTTPS and package downloads |

Docker publishes ports through its own `iptables` rules, so a host firewall such as UFW does not filter them. A provider firewall does. The AWS, Google Cloud and Azure templates create these rules for you; see [Cloud VMs](https://docs.grovs.io/docs/self-hosting/clouds).

## Secrets and `.env`

`.env` holds every credential and the keys that encrypt data at rest. Setup creates it readable by its owner only. On cloud VMs the bootstrap also locks `/opt/grovs` to root. Check it:

```bash
ls -l .env      # -rw------- expected
chmod 600 .env
```

Keep an encrypted copy off the server with your [backups](https://docs.grovs.io/docs/self-hosting/operate/backups). Never commit `.env` to a repository or paste it into a ticket.

### Rotate secrets

Edit the value in `.env`, then apply it with `docker compose --profile standalone up -d`. Generate new random values the way setup does, for example `openssl rand -hex 24`.

| Variable                                                                                                                                                | Rotate?                                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `ADMIN_API_KEY`, `DIAGNOSTICS_API_KEY`, `SENT_QUOTAS_WEBHOOK_KEY`                                                                                       | Yes. Update any monitoring that sends `DIAGNOSTICS_API_KEY`.                                              |
| `OAUTH_CLIENT_UID`, `OAUTH_CLIENT_SECRET`                                                                                                               | Yes. `migrate` updates the dashboard's OAuth application and the dashboard reads the new pair at start.   |
| `SMTP_PASSWORD`, `AWS_S3_KEY_ID`, `AWS_S3_ACCESS_KEY`                                                                                                   | Yes. Rotate them at the provider first.                                                                   |
| `SECRET_KEY_BASE`, `ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY`, `ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY`, `ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT` | **Never.** Existing data is bound to them, and changed encryption keys make encrypted columns unreadable. |
| `POSTGRES_PASSWORD`, `CLICKHOUSE_PASSWORD`                                                                                                              | **Never** after the first start. The databases keep the original password.                                |

> **Warning:** Losing `.env` means losing the encryption keys. A database backup without the matching `.env` cannot be fully restored.

## TLS

The standalone Caddy proxy issues Let's Encrypt certificates for the fixed hosts at startup and for each project link host on its first request. Expiry notices go to `ACME_EMAIL`.

On-demand issuance accepts any hostname whose DNS points at your server. For a hardened setup, issue wildcard certificates for the links and test links domains through your DNS provider's API instead, which also makes Universal Links and App Links reliable. This needs a Caddy build with your DNS provider's module (see the [Caddy DNS challenge docs](https://caddyserver.com/docs/automatic-https#dns-challenge)) or your own proxy. Behind Cloudflare, keep link records DNS-only. See [TLS and Universal Links](https://docs.grovs.io/docs/self-hosting/get-started/deployment#tls-and-universal-links).

## Admin accounts

- Public sign-up is off on self-hosted installations. New members join only through invites.
- The seed creates the bootstrap admin from `BOOTSTRAP_ADMIN_EMAIL` and `BOOTSTRAP_ADMIN_PASSWORD` only when that account does not exist. Changing `.env` later does not change the password.
- Set up [email](https://docs.grovs.io/docs/self-hosting/configure/configuration#email), then reset the bootstrap admin's password from the dashboard's **Account** page. Then clear `BOOTSTRAP_ADMIN_PASSWORD` in `.env`.
- Turn on two-factor authentication for every account on the **Account** page.
- Remove members who no longer need access.

## Updates

| Component                                      | How to update                                                                                  |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Grovs backend and dashboard                    | [Upgrades](https://docs.grovs.io/docs/self-hosting/operate/upgrades)                           |
| PostgreSQL, Redis, ClickHouse and Caddy images | `docker compose --profile standalone pull`, then `up -d`. The upgrade procedure does this too. |
| Ubuntu packages and Docker                     | `sudo apt-get update && sudo apt-get upgrade`, or enable Ubuntu's unattended upgrades          |

After a reboot, Docker restarts the containers, and on cloud VMs `grovs.service` starts the stack. Check [Monitoring](https://docs.grovs.io/docs/self-hosting/operate/monitoring) after every update.
