Deployment guide

Ontime 4.15.1

Overview

This guide shows you how to launch and use the Ontime 4.15.1 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud media servers listing on AWS Marketplace. You select it as the software version when you subscribe and launch.

Ontime is an open-source rundown manager and show timer for live events, broadcast and theater. You build the running order of a show in the browser editor, and Ontime keeps every screen in the venue on the same clock. The views include a large stage timer for presenters, a cuesheet for the show caller, a backstage view for the crew, a studio clock and public schedule views.

Companion, vMix, OBS, QLab and scripts can drive Ontime over its HTTP and WebSocket API.

What you get:

  • Built from the upstream 4.15.1 source. SecureAnyCloud builds Ontime from the checksum-verified v4.15.1 release tag (commit b5580040) with upstream's own build. The upstream code is unmodified. The one change is a dependency raise that lifts the vulnerable versions of twelve packages that the 4.15.1 lockfile resolves (among them body-parser, qs, path-to-regexp, ws and axios) to their fixed releases, inside upstream's own version ranges. The exact diff and the raised lockfile ship on the image.
  • A per-instance password, created before any port opens. First boot generates the Ontime password for your instance and writes it, with the API token derived from it, to /root/ontime-credentials.txt. Ontime and nginx refuse to start until that has happened. Everything except the login page and the health checks needs the password: the editor, every display view, the data and integration APIs and the WebSocket.
  • HTTPS only. nginx serves Ontime on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS and answers /healthz for load balancers. The session cookie is Secure and SameSite=Strict, and a login can redirect only to a page on your own server. A request that another website makes in your browser can never act on your session. Ontime's control API changes the show on a plain GET, so nginx refuses any cross-site request that carries the session cookie.
  • Ontime's own port is closed to the network. Ontime listens on port 4001 on every address, and a firewall rule drops that port on every interface except loopback, so the only way in is the HTTPS front. OSC input is off until you list the addresses allowed to send it.
  • A sandboxed service that cannot reach inside your network. Ontime runs as its own unprivileged account in a strict systemd sandbox. A firewall table loaded at every boot stops that account from opening connections to the EC2 instance metadata service, to private and reserved address ranges, and to your VPC's own IPv4 and IPv6 ranges. An HTTP automation URL therefore cannot be used to reach your internal services. If the instance cannot read its VPC ranges from instance metadata at boot, Ontime stays stopped and the firewall service retries until it can.
  • Fully patched. Ubuntu 24.04 LTS is patched at build time, IMDSv2 is required, and unattended security upgrades are switched on.

What is on the image: Ontime 4.15.1 in /opt/ontime/app with its own Node.js 24.22.2 runtime in /opt/ontime/node (no npm or other package manager), nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account that is subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet
  • A security group that allows:
    • TCP 22 (SSH) from your own networks only
    • TCP 443 (Ontime over HTTPS) from the networks of your operators and of every screen that shows a view
    • TCP 80 (redirect to HTTPS and /healthz) from the same sources as 443

Ontime is light, and an m5.large drives many display screens. Every screen needs to reach port 443. For a venue network, the usual setup is a security group that admits port 443 from the venue's public address.

1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Select Ontime 4.15.1 on Ubuntu 24.04 as the software version and select your Region. Then choose Launch through EC2.

Choose m5.large, your key pair and a security group as described above.

To launch with the AWS CLI instead, replace the values in angle brackets:

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=ontime}]'
2

Connect to your instance

Version SSH user

Ontime 4.15.1 on Ubuntu 24.04 ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

On first boot, the instance generates the certificate and the password. It then starts the firewall service, Ontime and nginx. Check the services:

systemctl is-active ontime-egress-guard ontime nginx
sudo test -f /var/lib/sac/data/ontime-firstboot.done && echo FIRSTBOOT_DONE curl -s --cacert /etc/ssl/ontime/ontime.crt --resolve "$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt):443:127.0.0.1" "https://$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt)/healthz"

All three units are active, the first-boot marker is present and the HTTPS front answers its health check:

active active active FIRSTBOOT_DONE ok

nginx serves Ontime on 80 and 443. Ontime itself listens on 4001 (its process shows as MainThread), and the firewall drops that port on every interface except loopback:

sudo ss -lntpH | awk '{split($6, a, "\""); print $4, a[2]}' | sort -u
0.0.0.0:22 sshd 0.0.0.0:4001 MainThread 0.0.0.0:443 nginx 0.0.0.0:80 nginx 127.0.0.53%lo:53 systemd-resolve 127.0.0.54:53 systemd-resolve [::]:22 sshd [::]:443 nginx [::]:80 nginx
4

Get your login details

sudo grep -E '^ONTIME_(HOST|URL)=' /root/ontime-credentials.txt
sudo stat -c '%a %U %n' /root/ontime-credentials.txt
ONTIME_HOST=ec2-203-0-113-10.compute-1.amazonaws.com ONTIME_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/ 600 root /root/ontime-credentials.txt

ONTIME_HOST is the instance's public DNS name from EC2 instance metadata. The certificate also names the public and private IP addresses. Open Ontime by that name, because a request for any other name (including the bare IP address) is refused. To use your own DNS name, see Step 9.

The full file, which also holds ONTIME_PASSWORD and the ONTIME_API_TOKEN derived from it, is readable by root only. Show it when you need the values:

sudo cat /root/ontime-credentials.txt

Keep the password and the token secret, because either one controls your show.

5

Log in and open the editor

Browse to the ONTIME_URL from Step 4 and accept the self-signed certificate warning. Step 9 shows how to use your own name and certificate. Ontime shows its login page. Enter your ONTIME_PASSWORD.

You land on the editor. Ontime creates a demo project on its first start, so the rundown already holds a few example events. Replace them with your own, or create a new project from the menu:

Use the playback controls to start the first event. Your browser keeps the session for later visits. Open Ontime by typing its address or from a bookmark. A link to it followed from another website arrives as a cross-site request and is refused (see Step 11).

6

The display views

Every view counts to the same clock. Open one on each screen you want to drive, and log in with the same password. The timer view at https://<host>/timer shows the presenter a large countdown with the current and next events:

The cuesheet at https://<host>/cuesheet gives the show caller the full rundown with start, end and duration for every cue:

Other views include /backstage, /studio, /timeline, /countdown and /info. All of them receive live updates over the WebSocket at wss://<host>/ws, which nginx forwards for you.

For a screen where nobody can type a password, use Ontime's share link feature. It creates a view address that carries a token (?token=). Anyone who holds that link can open the view, so share it only with the screens that need it.

7

Self test and the API

The image includes a self-test that checks the whole front from the instance itself. It checks the following:

  • The health checks pass.
  • Anonymous requests to the API, the data API and the WebSocket are refused.
  • A wrong password is refused.
  • A login gives a Secure and SameSite=Strict cookie and can redirect only to this server.
  • Cross-origin and cross-site requests are refused.
  • The session and the token can use the API.

When no show is running, the self-test also starts the first event, checks that the timer advances and stops it again. Run it with:

sudo ontime-selftest

The last line summarizes every check:

OK Ontime self-test passed: HTTPS front, anonymous API/data 401, view -> /login, anonymous WebSocket closed 1008, wrong password 401, login 302 with a Secure SameSite=Strict cookie and a host-confined redirect, session + token API 202, cross-origin POST, cross-site API GET and WebSocket 403, session WebSocket streams; timer ran (3598972 -> 3596000 ms) and was stopped

Integrations send the API token as a Bearer header. This example reads the token into a shell variable without printing it, asks Ontime for its version and reads the timer state:

H=$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt)
TOK=$(sudo sed -n 's/^ONTIME_API_TOKEN=//p' /root/ontime-credentials.txt)
curl -s --cacert /etc/ssl/ontime/ontime.crt --resolve "$H:443:127.0.0.1" -H "Authorization: Bearer $TOK" "https://$H/api/version"; echo
curl -s --cacert /etc/ssl/ontime/ontime.crt --resolve "$H:443:127.0.0.1" -H "Authorization: Bearer $TOK" "https://$H/api/poll" | python3 -c 'import json, sys; t = json.load(sys.stdin)["payload"]["timer"]; print("playback:", t["playback"])' unset TOK
{"payload":"4.15.1"} playback: stop

The control endpoints work the same way. Examples are /api/start, /api/pause, /api/stop, /api/roll and /api/load/index/2. The Ontime documentation describes the full API and the Companion module.

8

HTTP automations and OSC

Ontime's automations can call a URL or send OSC when an event starts or ends. Two things to know:

  • Targets must be public. The firewall table described in the Overview refuses connections from Ontime to the instance metadata service, to private and reserved address ranges and to your VPC. To let Ontime reach a private address (a lighting desk on your VPN, for example), add it to ONTIME_EGRESS_ALLOW in /etc/ontime/firewall.conf and run sudo systemctl restart ontime-egress-guard. You can never allow the metadata service.
  • OSC input is off. To let a desk send OSC to Ontime, enable OSC input in Ontime's settings. List the desk's address in ONTIME_OSC_IN_ALLOW_FROM in the same file, restart ontime-egress-guard and open UDP 8888 from that address in your security group.
9

Use your own DNS name and certificate

Point a DNS record at the instance. An Elastic IP keeps the address stable. Then tell the instance its name. This command regenerates the self-signed certificate for that name, updates the credentials file and reloads nginx:

sudo ontime-set-hostname ontime.your-domain.example

To use a certificate from a public CA instead, replace /etc/ssl/ontime/ontime.crt (full chain) and /etc/ssl/ontime/ontime.key (mode 0600) and run sudo systemctl reload nginx.

10

Change the password

To replace the password, for example after a staff change, run the rotation helper. It writes a new password and API token to Ontime and the credentials file, then restarts Ontime. Every browser, screen and integration then has to log in again or use the new token:

sudo ontime-rotate-credentials
11

Security checks

These checks show the protections described in the Overview on your own instance. First, check the front. An anonymous API request is refused, plain HTTP redirects to HTTPS, and the firewall accepts Ontime's port 4001 only on the loopback interface:

H=$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt) C="--cacert /etc/ssl/ontime/ontime.crt --resolve $H:443:127.0.0.1" curl -s $C -o /dev/null -w 'https://<host>/api/poll without a login -> HTTP %{http_code}\n' "https://$H/api/poll" curl -s --resolve "$H:80:127.0.0.1" -o /dev/null -w 'http://<host>/ -> HTTP %{http_code}\n' "http://$H/"
sudo nft list chain inet sac_ontime_egress input | grep -E 'iifname|dport 4001' | sed -E -e 's/counter packets [0-9]+ bytes [0-9]+ //' -e 's/^[[:space:]]+//'
https://<host>/api/poll without a login -> HTTP 401 http://<host>/ -> HTTP 301 iifname "lo" accept tcp dport 4001 drop comment "sac-ontime-port"

From any other machine, curl http://<instance-address>:4001/ gets no answer. (On the instance itself, traffic to its own addresses travels over loopback, so test this from another host.) Next, cross-site requests. A web page on another site can make your browser send a request to Ontime, and the browser marks it Sec-Fetch-Site: cross-site (or same-site from a sibling domain). nginx refuses every such request that carries a session cookie, on every path and every spelling of it, including a ?token= that is not a real share link token; a request with no cookie gets the normal login answer:

H=$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt) C="--cacert /etc/ssl/ontime/ontime.crt --resolve $H:443:127.0.0.1" for p in /api/stop /API/stop /api//stop '/api/stop?token=junk' /data/db; do curl -s $C -o /dev/null -H 'Sec-Fetch-Site: cross-site' -H 'Cookie: token=any' -w "cross-site $p with a session cookie -> %{http_code}\n" "https://$H$p" done curl -s $C -o /dev/null -H 'Sec-Fetch-Site: same-site' -H 'Cookie: token=any' -w 'same-site /api/stop with a session cookie -> %{http_code}\n' "https://$H/api/stop" curl -s $C -o /dev/null -H 'Sec-Fetch-Site: cross-site' -w 'cross-site /api/stop without a cookie -> %{http_code}\n' "https://$H/api/stop"
cross-site /api/stop with a session cookie -> 403 cross-site /API/stop with a session cookie -> 403 cross-site /api//stop with a session cookie -> 403 cross-site /api/stop?token=junk with a session cookie -> 401 cross-site /data/db with a session cookie -> 403 same-site /api/stop with a session cookie -> 403 cross-site /api/stop without a cookie -> 401

With a ?token=, the cookie is removed before the request reaches Ontime, so the token has to be valid on its own: a junk one gets 401 and a real share link token works. Next, the logs: query strings (where share link tokens travel) are never written, the error log records only critical errors, and the log directory is closed to other accounts:

sudo nginx -T 2>/dev/null | grep -c 'error_log /var/log/nginx/error.log crit;'
sudo nginx -T 2>/dev/null | grep -m1 'log_format sac_ontime' stat -c '%a %U:%G %n' /var/log/nginx
4 log_format sac_ontime '$remote_addr - [$time_local] "$request_method $uri $server_protocol" $status $body_bytes_sent "$http_user_agent"'; 750 root:www-data /var/log/nginx

Next, the egress sandbox: the ontime account cannot reach the instance metadata service or the instance's own private address, the firewall table holds your VPC's ranges, and the firewall service retries until it has them:

sudo -u ontime curl -s -m 5 -o /dev/null -w 'uid ontime -> instance metadata: %{http_code}\n' http://169.254.169.254/latest/meta-data/ || true
sudo -u ontime curl -s -m 5 -o /dev/null -w "uid ontime -> this instance's private address: %{http_code}\n" "http://$(hostname -I | awk '{print $1}')/healthz" || true
sudo nft list table inet sac_ontime_egress | grep -oE 'comment "sac-[a-z0-9-]+"' | sort -u grep -E '^vpc[46]=' /run/sac/ontime-egress-vpc systemctl show ontime-egress-guard -p Restart --value systemctl show ontime nginx -p IPAddressDeny --value
uid ontime -> instance metadata: 000 uid ontime -> this instance's private address: 000 comment "sac-allow" comment "sac-allow6" comment "sac-metadata" comment "sac-metadata6" comment "sac-multicast" comment "sac-multicast6" comment "sac-ontime-port" comment "sac-osc-allow" comment "sac-osc-allow6" comment "sac-osc-in" comment "sac-private" comment "sac-private6" comment "sac-vpc" comment "sac-vpc6" vpc4=172.31.0.0/16 vpc6=none on-failure 169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128

The vpc rules hold your VPC's IPv4 and IPv6 ranges (none when the VPC has no IPv6 block), read from instance metadata at every boot. Finally, the build: Ontime reports 4.15.1, it runs as its own account in a strict sandbox, there is no package manager on the image, and Ubuntu's shared snakeoil certificate service is masked:

systemctl show ontime -p User -p ProtectSystem -p NoNewPrivileges --value command -v npm pnpm npx corepack || echo 'no package manager on the PATH'
systemctl is-enabled ssl-cert.service || true
ontime strict yes no package manager on the PATH masked

Maintenance

  • Data: Ontime keeps its projects, settings and logs in /var/lib/ontime/data, readable by the ontime account only. Export a project from the editor menu to keep a copy elsewhere.
  • Logs: journalctl -u ontime for Ontime, /var/log/nginx/ for the HTTPS front.
  • Health checks: GET /healthz on port 80 or 443 returns ok without a login.
  • Configuration guard: Ontime does not start if first boot has not run, if the password is missing or weak, or if the firewall table is not loaded. journalctl -u ontime-egress-guard shows why when it is waiting.
  • Security updates: Unattended security upgrades are on and cover nginx and every system library. Reboot when a new kernel is installed. Ontime and its Node.js runtime are updated by a new version of this image.

Licenses and source code

Ontime is licensed under the GNU General Public License version 3 or later. It is built from the upstream 4.15.1 source. The only change is the dependency raise. The overrides, the lockfile diff and the raised lockfile are in /usr/share/doc/ontime-sac/. The corresponding source (ontime-v4.15.1-source.tar.gz), the license notice and full text, the Node.js license and the licenses of the bundled third-party packages (third-party-licenses.json) are in the same directory, with build details in README.sac.

Support

SecureAnyCloud provides 24/7/365 expert technical support for this image. Contact sales@secureanycloud.com. This image is provided by SecureAnyCloud and is not affiliated with or endorsed by the Ontime project.

Deploy on AWS

Launch Ontime 4.15.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS