Remote bells and webhooks

School Bell supports three different remote-control paths. Choose the smallest one that matches the installation:

Feature

Purpose

Top-level trigger

Copy every scheduled signal to another Linux bell over SSH.

manual_bell.remote_bells

Start best-effort SSH commands or webhooks when the physical button is pressed.

Top-level webhook

Accept authenticated requests that ring this node locally.

Remote connectivity is optional. A failed remote action is logged but does not make a correctly configured local bell dependent on a central service.

Prepare SSH access

SSH actions must work non-interactively as the School Bell service user. On the sending Pi, create a dedicated key and copy its public key to the receiving Pi:

$ ssh-keygen -t ed25519 -C school-bell -f /home/pi/.ssh/id_school_bell
$ ssh-copy-id -i /home/pi/.ssh/id_school_bell.pub pi@pibell-yard.local

Protect the private key and do not commit it. A service cannot answer a key passphrase prompt, so use a deliberately protected service key and restrict the receiving account and network access appropriately.

Create /home/pi/.ssh/config on the sender:

Host pibell-yard
    HostName pibell-yard.local
    User pi
    IdentityFile /home/pi/.ssh/id_school_bell
    IdentitiesOnly yes
    PreferredAuthentications publickey

Connect once and verify the host identity before unattended operation:

$ ssh pibell-yard /usr/bin/aplay --help

Run this test as the same account used by school-bell.service. It must complete without asking for a password, passphrase or host confirmation.

Warning

The current built-in SSH command disables strict host-key checking for compatibility with unattended installations. Register and verify the host yourself, restrict the dedicated key on the receiving Pi, and only use SSH triggering over a trusted management network. Host-key pinning should be added before treating an untrusted network as safe.

Mirror scheduled signals over SSH

The legacy top-level trigger mapping sends every scheduled signal to each listed host. Its value is the WAVE directory on that remote host:

{
  "trigger": {
    "pibell-yard": "/home/pi/samples"
  }
}

The remote directory must contain the same WAVE filenames selected by the local wav mapping, and /usr/bin/aplay must be available remotely. Test every destination before adding it to a production schedule.

Remote actions from a physical button

manual_bell.remote_bells can execute a specific SSH command or call an HTTP(S) webhook when the physical input is accepted:

{
  "manual_bell": {
    "gpio": 17,
    "wav_key": "lesson",
    "remote_bells": [
      {
        "transport": "ssh",
        "host": "pibell-yard.local",
        "user": "pi",
        "command": ["/usr/local/bin/manual-bell"],
        "timeout": 10
      },
      {
        "transport": "webhook",
        "url": "https://remote-bell.example.com/bell",
        "auth": {
          "type": "bearer",
          "token": "replace-with-remote-token"
        },
        "timeout": 5
      }
    ]
  }
}

These actions start in background threads and do not delay the local signal. Their success or failure is reported as manual_remote_trigger. A failed remote action does not undo or change the local result.

On a trusted local network, prefer a stable local IP address such as http://192.168.1.25:8081/bell when hostname or mDNS resolution adds noticeable latency. A hostname is more readable and easier to renumber, but it depends on reliable local name resolution. Using an IP address avoids that lookup; it does not compensate for a slow HTTP response from the receiving service.

Expose a bell webhook

Enable the authenticated endpoint on the receiving node:

{
  "webhook": {
    "enabled": true,
    "host": "127.0.0.1",
    "port": 8081,
    "token": "replace-with-a-long-random-token",
    "rate_limit": 10,
    "rate_window": 60
  }
}

The built-in server provides plain HTTP. Keep it on 127.0.0.1 and expose it through a TLS reverse proxy such as nginx, Caddy or Apache. Configure certificate validation, request-size limits and trusted-network access at the proxy. Never expose the built-in port directly to an untrusted network.

Manual HTTPS with Caddy’s local CA

Caddy can provide HTTPS without a public domain or internet-facing service. It creates a local certificate authority (CA), issues and renews the server certificate, and proxies requests to School Bell over loopback. Install Caddy using the official installation instructions, then create or extend /etc/caddy/Caddyfile on the receiving node:

pibell-yard.school-bell.internal {
    tls internal
    request_body {
        max_size 4KB
    }
    reverse_proxy 127.0.0.1:8081
}

Use a hostname that resolves to the receiving node from every sending node. Create an A record in the internal DNS, for example pibell-yard.school-bell.internal pointing to the node’s fixed DHCP address. Do not create a CNAME whose target is an IP address. Avoid .local because it is reserved for mDNS. A controlled /etc/hosts entry can be used as a fallback, but internal DNS is preferable. The hostname in the webhook URL must match the hostname in the Caddyfile.

Install DNS client tools when dig is not already available:

$ sudo apt install bind9-dnsutils
$ dig +noall +answer pibell-yard.school-bell.internal A
$ getent ahostsv4 pibell-yard.school-bell.internal

The answer must be an A record containing the fixed IPv4 address. If multiple DNS servers are supplied through DHCP, every one of them must return the same private record. Do not advertise a public resolver as a fallback for private names.

Validate and load the configuration:

$ sudo caddy validate --config /etc/caddy/Caddyfile
$ sudo systemctl reload caddy

When Caddy runs as a systemd service, its local root certificate is normally stored at:

/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt

Install a copy of this public root certificate in the trust store of every sending node. Do not copy any private key from Caddy’s data directory. On a Debian or Raspberry Pi OS sender, copy the certificate securely to the node and run:

$ sudo install -m 0644 root.crt \
    /usr/local/share/ca-certificates/school-bell-caddy.crt
$ sudo update-ca-certificates

Compare openssl x509 -noout -fingerprint -sha256 output before and after copying the root certificate. This verifies that the expected trust anchor is being installed.

Python requests in the School Bell virtual environment may use its own CA bundle instead of the operating-system store used by curl. If curl succeeds but a remote bell reports SSLError, add a systemd drop-in on the sending node:

$ sudo systemctl edit school-bell
[Service]
Environment="REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt"

Then reload systemd and restart School Bell:

$ sudo systemctl daemon-reload
$ sudo systemctl restart school-bell

The bearer token remains required after enabling HTTPS. Test the complete path from a sending node, without disabling certificate verification:

$ curl -X POST https://pibell-yard.school-bell.internal/bell \
    -H 'Authorization: Bearer replace-with-a-long-random-token' \
    -H 'Content-Type: application/json' \
    --data '{"wav_key":"lesson"}'

Keep Caddy’s data directory persistent because it contains the local CA. Back up and protect it like other private key material. All proxied requests reach the built-in rate limiter from the loopback address, so its limit is shared by all clients using this proxy. The external URL uses the default HTTPS port 443; do not retain the backend :8081 port when changing a remote URL from HTTP to HTTPS.

Automate Caddy with Ansible

The optional ansible/caddy.yml playbook installs Caddy and DNS diagnostic tools, deploys a host-specific certificate and key, installs the public CA in the operating-system trust store, configures the School Bell systemd service to use that store, and performs DNS, TLS and Python requests checks. HTTPS is not enabled by init.yml or install.yml.

For managed deployments, issue every node a separate certificate from one controlled internal CA. Keep the CA private key outside the repository and off the bell nodes. Supply only the public CA certificate and each node’s own leaf certificate and private key to the playbook. See Deploying with Ansible for the inventory variables and rollout command. This differs deliberately from the manual tls internal example: the manual procedure lets one Caddy instance own its local CA, while the managed procedure uses centrally issued certificates so no CA private key is distributed to bell nodes.

Send exactly one configured WAVE key:

$ curl -X POST https://bell.example.com/bell \
    -H 'Authorization: Bearer replace-with-a-long-random-token' \
    -H 'Content-Type: application/json' \
    --data '{"wav_key":"lesson"}'

HTTP status

Meaning

202

The local signal was accepted for background execution.

400

The JSON or wav_key is invalid.

401

Bearer authentication failed.

404

The request path is not /bell.

405

The endpoint only accepts POST.

409

Another signal is already active.

429

The per-client rate limit was exceeded.

503

Background execution could not be started.

Responses contain keys and status information, never sample paths. Webhook signals return 202 immediately after atomic acceptance; playback completion or failure is reported through the webhook_bell_* monitoring events. They are manual overrides: they do not respect holiday or disable-calendar suppression and they do not automatically invoke the legacy top-level SSH trigger destinations.

Verify and diagnose

  1. Run school-bell /home/pi/schema.json --check on every node. This checks remote configuration structure, but deliberately makes no remote SSH or webhook connection.

  2. Test local audio and GPIO before adding a remote path.

  3. Test SSH as the systemd service user or test the webhook through its final TLS URL.

  4. If curl succeeds but the service reports SSLError, inspect REQUESTS_CA_BUNDLE in systemctl show school-bell.

  5. Trigger one bell while another signal is active and confirm the expected 409 or skipped event.

  6. Inspect journalctl and the Graylog manual_remote_trigger, remote_trigger or webhook_* events.

See Configuration reference for every field and Networking and time synchronization for firewall and outage behavior.