> For the complete documentation index, see [llms.txt](https://docs.k9.io/key9-identity/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.k9.io/key9-identity/jsonair/2-install/2.8-admin-web-interface.md).

# 2.8 Admin Web Interface

`jsonair-admin` is a lightweight browser-based administration tool for managing the `configurations` table. It provides a login-protected UI where administrators can search, create, edit, and delete configuration records without needing direct database access or the `jsonair-encrypt` command-line tool.

All configuration data written through the admin interface is automatically base64-encoded and encrypted using the same AES-256-GCM method that the JSONAir server uses, so agents retrieve configurations transparently with no changes on their side.

***

## How It Works

| Step | What Happens                                                                                                               |
| ---- | -------------------------------------------------------------------------------------------------------------------------- |
| 1    | Admin logs in with a username and password stored in environment variables                                                 |
| 2    | A signed JWT session cookie is issued (4-hour lifetime, HTTP-only)                                                         |
| 3    | Admin searches the `configurations` table by type and/or name                                                              |
| 4    | Admin opens a record — the existing `config_data` is decrypted and base64-decoded for display                              |
| 5    | Admin edits the raw configuration text and saves                                                                           |
| 6    | On save, the admin interface base64-encodes the raw text, encrypts it with AES-256-GCM, and writes it back to the database |
| 7    | The JSONAir server decrypts and serves the updated configuration to agents on the next request                             |

The admin interface shares the `CONFIG_ENCRYPT_SECRET` with the JSONAir server — this is what allows it to read and write encrypted `config_data` values that the server can then decrypt.

***

## Building

### With Make

```bash
make jsonair-admin
```

The binary is placed at:

```
bin/jsonair-admin/jsonair-admin
```

### Directly with Go

```bash
go build -o bin/jsonair-admin/jsonair-admin ./cmd/jsonair-admin
```

The HTML templates are embedded into the binary at compile time using Go's `embed` package. No external template files are needed at runtime — the binary is fully self-contained.

***

## Environment Variables

### Required — Admin Credentials

| Variable               | Description                                                                                          | Example                |
| ---------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------- |
| `ADMIN_USERNAME`       | Login username for the web interface                                                                 | `admin`                |
| `ADMIN_PASSWORD`       | Login password for the web interface                                                                 | `openssl rand -hex 16` |
| `ADMIN_SESSION_SECRET` | Secret used to sign session JWTs. Use a long, random string — keep this separate from other secrets. | `openssl rand -hex 32` |

### Required — Encryption

| Variable                | Description                                                                                                                                                                                                                                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `CONFIG_ENCRYPT_SECRET` | **Must match the value used by the JSONAir server.** This is the same secret used to derive the AES-256-GCM key for `config_data`. If this value differs from the server's, the admin interface will not be able to decrypt existing records and the server will not be able to read records written by the admin. |

### Required — Database

| Variable         | Description             | Example            |
| ---------------- | ----------------------- | ------------------ |
| `MYSQL_USERNAME` | Database username       | `jsonair`          |
| `MYSQL_PASSWORD` | Database password       | `your-db-password` |
| `MYSQL_DATABASE` | Database name           | `jsonair`          |
| `MYSQL_HOST`     | Database hostname or IP | `127.0.0.1`        |
| `MYSQL_PORT`     | Database port           | `3306`             |

### Optional — Database TLS

| Variable                | Description                                                                        | Default |
| ----------------------- | ---------------------------------------------------------------------------------- | ------- |
| `MYSQL_TLS`             | Set to `true` to enable TLS for the database connection                            | `false` |
| `MYSQL_TLS_SKIP_VERIFY` | Set to `true` to disable certificate verification. Not recommended for production. | `false` |

### Optional — HTTP Server

| Variable      | Description                                                      | Default |
| ------------- | ---------------------------------------------------------------- | ------- |
| `HTTP_LISTEN` | Address and port to listen on                                    | `:8080` |
| `HTTP_TLS`    | Set to `true` to enable HTTPS                                    | `false` |
| `HTTP_CERT`   | Path to the TLS certificate file. Required when `HTTP_TLS=true`. | —       |
| `HTTP_KEY`    | Path to the TLS private key file. Required when `HTTP_TLS=true`. | —       |

When `HTTP_TLS=true`, the admin interface serves HTTPS directly. Alternatively, leave TLS disabled and terminate it at a reverse proxy (nginx, Caddy, etc.).

***

## Example `.env` File

```ini
# Admin credentials
ADMIN_USERNAME=admin
ADMIN_PASSWORD=replace-with-a-strong-password
ADMIN_SESSION_SECRET=replace-with-output-of-openssl-rand-hex-32

# Must match the JSONAir server value exactly
CONFIG_ENCRYPT_SECRET=same-value-as-your-jsonair-server

# Database (same database as the JSONAir server)
MYSQL_USERNAME=jsonair
MYSQL_PASSWORD=your-strong-db-password
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_DATABASE=jsonair

# HTTP
HTTP_LISTEN=:8080

# HTTPS (optional — remove if using a reverse proxy for TLS termination)
# HTTP_TLS=true
# HTTP_CERT=/etc/letsencrypt/live/admin.example.com/fullchain.pem
# HTTP_KEY=/etc/letsencrypt/live/admin.example.com/privkey.pem
```

> **Security:** Never commit a `.env` file containing real credentials to source control. Add `.env` to your `.gitignore`.

***

## Running the Admin Interface

### Directly

Place a `.env` file in the same directory as the binary and run:

```bash
cd /opt/jsonair-admin
./jsonair-admin
```

### As a systemd Service

Create `/etc/systemd/system/jsonair-admin.service`:

```ini
[Unit]
Description=JSONAir Admin Web Interface
After=network.target mysql.service
Wants=mysql.service

[Service]
Type=simple
User=jsonair-admin
WorkingDirectory=/opt/jsonair-admin
EnvironmentFile=/etc/jsonair/admin.env
ExecStart=/opt/jsonair-admin/jsonair-admin
Restart=on-failure
RestartSec=5
KillMode=mixed
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target
```

Place your environment variables in `/etc/jsonair/admin.env`, then enable and start the service:

```bash
sudo systemctl daemon-reload
sudo systemctl enable jsonair-admin
sudo systemctl start jsonair-admin
sudo systemctl status jsonair-admin
```

### With HTTPS

Set the three TLS variables and the admin interface will serve HTTPS directly — no reverse proxy required.

```ini
HTTP_TLS=true
HTTP_CERT=/etc/letsencrypt/live/admin.example.com/fullchain.pem
HTTP_KEY=/etc/letsencrypt/live/admin.example.com/privkey.pem
HTTP_LISTEN=:8443
```

When TLS is enabled the server enforces TLS 1.2 as the minimum version, adds a `Strict-Transport-Security` header to every response, and marks the session cookie `Secure` so it is never transmitted over plain HTTP.

> **Tip:** Generate a self-signed certificate for internal use: `openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=jsonair-admin'`

### In a Container

Because all configuration is passed via environment variables and the binary is fully self-contained, the admin interface is straightforward to run in a container. A minimal `Dockerfile` looks like:

```dockerfile
FROM scratch
COPY jsonair-admin /jsonair-admin
ENTRYPOINT ["/jsonair-admin"]
```

Or using a minimal base image:

```dockerfile
FROM gcr.io/distroless/static-debian12
COPY jsonair-admin /jsonair-admin
ENTRYPOINT ["/jsonair-admin"]
```

Pass environment variables at runtime:

```bash
docker run -d \
  -p 8080:8080 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=your-password \
  -e ADMIN_SESSION_SECRET=your-session-secret \
  -e CONFIG_ENCRYPT_SECRET=your-encrypt-secret \
  -e MYSQL_USERNAME=jsonair \
  -e MYSQL_PASSWORD=your-db-password \
  -e MYSQL_HOST=db.internal \
  -e MYSQL_PORT=3306 \
  -e MYSQL_DATABASE=jsonair \
  jsonair-admin
```

> **Note:** Cross-compile for Linux when building on macOS: `GOOS=linux GOARCH=amd64 go build -o jsonair-admin ./cmd/jsonair-admin`

***

## Health Check

`jsonair-admin` exposes an unauthenticated health endpoint at `/health`. It pings the database and returns HTTP `200` when healthy or HTTP `503` when the database is unreachable.

```bash
curl http://localhost:8080/health
```

Healthy response:

```json
{"status": "ok"}
```

Unhealthy response (HTTP 503):

```json
{"status": "error", "message": "database unreachable"}
```

### Docker HEALTHCHECK

```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:8080/health || exit 1
```

Replace `8080` with the port set in `HTTP_LISTEN` if you change the default.

***

## Usage

### Logging In

Navigate to `http://your-host:8080` (or `https://` if `HTTP_TLS=true`) using the address set in `HTTP_LISTEN`. You will be redirected to the login page. Enter the `ADMIN_USERNAME` and `ADMIN_PASSWORD` values from your environment.

Sessions expire after 4 hours of inactivity. You will be redirected to the login page when your session expires.

### Searching Configurations

After login you land on the **Configurations** list page. All records from the `configurations` table are shown in a table with columns for Key UUID, Type, Name, Reload, Debug, and last-updated timestamp.

Use the **Type** and **Name** search fields at the top to filter the list. Filters are substring matches, so searching for `suri` in the Type field will match `suricata`. Both filters can be combined. Click **Clear** to reset.

### Creating a Configuration

Click **+ New Configuration** to open the creation form. Fill in the following fields:

| Field                  | Description                                                                                                                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Key (UUID)**         | The key UUID this configuration belongs to. Select from the dropdown — it lists all records in the `keys` table. The agent must authenticate with this key to retrieve the configuration. |
| **Type**               | The configuration type (e.g. `suricata`, `nginx`). Must match what the agent sends in its `JSONAIR_TYPE` variable.                                                                        |
| **Name**               | The configuration name (e.g. `suricata.yaml`). Must match what the agent sends in its `JSONAIR_NAME` variable.                                                                            |
| **Reload**             | Optional reload key or trigger value.                                                                                                                                                     |
| **Debug**              | Optional debug level (e.g. `INFO`, `DEBUG`).                                                                                                                                              |
| **Configuration Data** | Paste the raw configuration text here. Any format is accepted — YAML, JSON, TOML, plain text, binary, etc. The admin interface does not validate the content.                             |

Click **Create** to save. The raw text is base64-encoded and encrypted before being written to the database.

### Editing a Configuration

Click **Edit** next to any record in the list. The edit form shows the same fields as the creation form, pre-populated with the current values. The **Configuration Data** textarea displays the decrypted, decoded raw configuration text.

Make any changes and click **Save**. The updated text will be re-encoded and re-encrypted.

> **Note:** Changing the **Type** or **Name** of a configuration will break any agents currently polling for the old `type`/`name` combination. Update agents before changing these fields in production.

### Deleting a Configuration

Click **Delete** next to any record in the list. A browser confirmation dialog will appear before the record is permanently removed. Deletion cannot be undone.

***

## Security Considerations

* **Keep the admin interface off the public internet.** Run it on an internal network, behind a VPN, or behind an authenticated reverse proxy. It provides direct read and write access to all configuration data in the database.
* The session cookie is **HTTP-only** and **SameSite=Strict** to mitigate XSS and CSRF attacks. When `HTTP_TLS=true`, the cookie is also marked **Secure** so it is never transmitted over plain HTTP. Either enable TLS directly or place the admin interface behind a TLS-terminating reverse proxy (nginx, Caddy, etc.) when running over an untrusted network.
* `ADMIN_SESSION_SECRET`, `ADMIN_PASSWORD`, and `CONFIG_ENCRYPT_SECRET` should each be distinct values generated with `openssl rand -hex 32`.
* The admin interface does not drop OS privileges at startup (unlike the main server). Run it as a dedicated low-privilege user.
