Deployment guide

Cronicle Job Scheduler 0.9.137

Overview

This guide shows how to launch and use the Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. The AMI is one software version of the SecureAnyCloud Coolify and Container Platforms and PaaS listing on AWS Marketplace. Select it as the software version when you subscribe and launch.

Cronicle is a job scheduler and cron replacement with a web interface. You define events, each made up of a shell script, an HTTP request or your own plugin plus a schedule. Cronicle runs them, retries and chains them, enforces timeouts and resource limits, keeps the history, and shows each running job's progress, CPU, memory and log live in the browser. Several people can share it, each with their own account and privileges. This image runs the official Cronicle 0.9.137 release with its built-in storage, so there is no separate database to run.

What you get:

  • HTTPS from the first boot. nginx serves Cronicle on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • An admin password and session secret unique to your instance. First boot creates Cronicle's storage and sets the admin password before Cronicle ever starts, so the upstream default admin / admin sign-in is never live. It also generates the secret that Cronicle signs sessions with. Nothing secret is baked into the image.
  • Fail closed. Cronicle refuses to start until first boot has finished, and so does nginx.
  • Cronicle on loopback only. Cronicle listens on 127.0.0.1:3012 behind nginx, including the WebSocket that streams live job logs. Cross-site requests and cross-site WebSocket connections are refused. The open ports are 22, 80 and 443.
  • Jobs run as an unprivileged user. Cronicle and every job it starts run as the cronicle system user (upstream runs them as root).
  • Protection against server-side request forgery. Cronicle's HTTP Request plugin fetches any URL you give it, so Cronicle, every job it runs and nginx are blocked from the instance metadata service at the kernel level. The AMI requires IMDSv2.
  • Your schedule on its own volume. Cronicle's storage (schedule, users, job history and logs) lives on a separate 20 GiB gp3 volume at /var/lib/cronicle that you can resize or snapshot on its own.
  • Hardened defaults. Cronicle's address comes from your configuration, never from a request. Client addresses come from the connection, not from headers that a client can forge. Requests for a host name that the instance does not serve are dropped.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Cronicle 0.9.137 (built from the release tag with its locked dependencies), Node.js 22 LTS, 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 (or a private route that you use for management).
  • A security group that allows inbound TCP 22 (administrator SSH), TCP 443 (Cronicle) and TCP 80 (redirects to HTTPS) from your own networks only.
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Select Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 as the software version, pick your Region, and choose Launch through EC2.

Select m5.large as the instance type, then choose your key pair and the security group described in Prerequisites. The AMI includes a second 20 GiB volume for Cronicle's data. You can make it larger on the launch page.

With the AWS CLI (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=cronicle}]'
2

Connect to your instance

Version SSH user

Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 ubuntu

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

Check the services

First boot takes a few seconds. When it finishes, the three units are active:

systemctl is-active cronicle-firstboot cronicle nginx
active active active

Cronicle (its process is called Cronicle Server) listens on loopback only, and nginx serves ports 80 and 443:

sudo ss -tlnpH | sed -E 's/^LISTEN +[0-9]+ +[0-9]+ +([^ ]+) .*users:\(\("([^"]+)".*/\1 \2/' | sort -u
0.0.0.0:22 sshd 0.0.0.0:443 nginx 0.0.0.0:80 nginx 127.0.0.1:3012 Cronicle Server 127.0.0.53%lo:53 systemd-resolve 127.0.0.54:53 systemd-resolve [::]:22 sshd [::]:443 nginx [::]:80 nginx

Check the Cronicle version, the release it was built from, and the Node.js runtime:

node -p "require('/opt/cronicle/package.json').version" cat /usr/share/doc/cronicle/SOURCE
node --version
0.9.137 Cronicle 0.9.137 source https://github.com/jhuckaby/Cronicle tag v0.9.137 commit 34fbd63058ac9f121e76527fded7211b366b996d v22.23.3
4

Get your URL and password

Read the credentials file:

sudo cat /root/cronicle-credentials.txt

The file holds the address that Cronicle is served on (your instance's public DNS name by default), the user name and the password generated on this instance:

# Cronicle 0.9.137 - generated on first boot by cronicle-firstboot.service
# This password is unique to this instance; none was baked into the image. Keep this file secret. CRONICLE_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ CRONICLE_USERNAME=admin CRONICLE_PASSWORD=<CRONICLE_PASSWORD>
#
# Open CRONICLE_URL in a browser (self-signed certificate minted on this instance's first boot; port 80 # redirects to https) and sign in with the user and password above. # Jobs run as the unprivileged 'cronicle' user. Schedule, users and job history: /var/lib/cronicle # Change the password:
sudo cronicle-set-password Change the served name:
sudo cronicle-set-hostname <name>
# User guide: https://secureanycloud.com/stacks/cronicle-job-scheduler-0-9-137/
5

Sign in from your browser

Open CRONICLE_URL in your browser. The certificate is self-signed (generated on your instance), so accept the browser warning once, or use your own name and certificate (Step 10). Sign in with admin and the password from Step 4.

The Home tab shows server statistics, the jobs running now and the upcoming scheduled runs. The clock and status at the top right update live over a WebSocket.

6

Create an event and watch a job run

Open Schedule, then Add Event.... Give the event a name, and then choose the General category, the Primary Group target (this server) and a plugin. The image includes three plugins:

  • Shell Script runs the script you type, as the cronicle user.
  • HTTP Request sends a GET, HEAD or POST request to a URL and records the response.
  • Test Plugin simulates a job, which is useful for trying things out.

Set the timing (or On Demand), then Create Event.

On the Schedule tab choose Run next to the event to start it now. Open the job from Home to see its progress, CPU and memory, and its log as it is written:

Finished jobs, with their result, duration and full log, are on the Completed tab:

7

Sign in from the command line

Cronicle's API answers every sign-in request with HTTP 200, and the JSON code says whether it worked. This example reads your password from the credentials file (it never appears on a command line), signs in, and then shows that the upstream default password is refused:

H=$(sudo sed -n 's#^CRONICLE_URL=https://\([^/]*\)/.*#\1#p' /root/cronicle-credentials.txt) login() { python3 -c 'import json,sys; print(json.dumps({"username": "admin", "password": sys.stdin.read().strip()}))' \
  | curl -sk --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -H "Origin: https://$H" \
  --data-binary @- "https://$H/api/user/login" \
  | python3 -c 'import json,sys; d=json.load(sys.stdin); print("signed in" if d.get("code") == 0 else "refused: " + d.get("description", ""))' } echo "this instance's password: $(sudo sed -n 's/^CRONICLE_PASSWORD=//p' /root/cronicle-credentials.txt | login)" echo "upstream default 'admin': $(echo admin | login)"
this instance's password: signed in upstream default 'admin': refused: Username or password incorrect.

Cronicle locks an account at the sixth failed sign-in within an hour, and unlocking it normally needs a password reset email. To unlock admin (or to change its password) on this instance, use cronicle-set-password (Step 11).

8

Where your data lives

Cronicle's storage (schedule, users, sessions, job history and job logs), its own logs and the job queue are on the separate data volume, linked from the install directory:

findmnt -no TARGET,SOURCE,FSTYPE,SIZE /var/lib/cronicle
sudo ls -l /opt/cronicle | awk '$1 ~ /^l/ {print $9, $10, $11}'
/var/lib/cronicle /dev/nvme1n1 ext4 19.5G data -> /var/lib/cronicle/data logs -> /var/lib/cronicle/logs queue -> /var/lib/cronicle/queue

Back up Cronicle by snapshotting this volume (stop Cronicle first for a consistent copy: sudo systemctl stop cronicle, snapshot, sudo systemctl start cronicle). Settings are in /opt/cronicle/conf/config.json.

9

Security checks

Port 80 sends browsers to HTTPS, a request from another website is refused, and nginx closes the connection for a host name that the instance does not serve (000 means no answer):

H=$(sudo sed -n 's#^CRONICLE_URL=https://\([^/]*\)/.*#\1#p' /root/cronicle-credentials.txt)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk -o /dev/null -w 'web interface: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'cross-site API call: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Origin: https://example.com' -H 'Content-Type: application/json' -d '{}' "https://$H/api/user/login"
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ web interface: 200 cross-site API call: 403 unknown host name: 000

Cronicle runs as the unprivileged cronicle user, and neither Cronicle (with every job it starts) nor nginx can reach the instance metadata service:

systemctl show cronicle -p User -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128 User=cronicle IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128

You can see the block from Cronicle itself: an HTTP Request event for http://169.254.169.254/latest/meta-data/ fails with Connect Timeout, while the same event for a public site such as https://example.com/ succeeds. A Shell Script job that tries curl http://169.254.169.254/ fails the same way.

Check the remaining settings. Cronicle listens on loopback, builds its links from the configured address, has no network discovery listener, and signs sessions with a secret generated on this instance:

sudo python3 -c 'import json; c=json.load(open("/opt/cronicle/conf/config.json")); w=c["WebServer"]; print("bind_address:", w["bind_address"]); print("base_app_url:", c["base_app_url"]); print("udp_broadcast_port:", c["udp_broadcast_port"]); print("secret_key: %d hex characters, generated on this instance" % len(c["secret_key"]))'
bind_address: 127.0.0.1 base_app_url: https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com udp_broadcast_port: 0 secret_key: 64 hex characters, generated on this instance

The certificate was generated on this instance and names its own addresses:

H=$(sudo sed -n 's#^CRONICLE_URL=https://\([^/]*\)/.*#\1#p' /root/cronicle-credentials.txt)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = sac X509v3 Subject Alternative Name: DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost
10

Use your own DNS name and certificate

Point a DNS record at the instance, and then set the name that Cronicle is served as. This command creates a new self-signed certificate for the name, updates nginx (and which sites may call the API), Cronicle's base_app_url (the links in notification emails and web hooks) and the URL in /root/cronicle-credentials.txt, restarts Cronicle, and keeps the name across reboots:

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

To use a certificate from a public authority instead, replace /etc/nginx/ssl/cronicle.crt and /etc/nginx/ssl/cronicle.key (keep the key mode 0600) and run sudo systemctl reload nginx. Running cronicle-set-hostname again replaces them with a new self-signed pair.

11

Change the admin password

Cronicle stops briefly while the password is changed, so make the change when no job is running. To generate a new random password (written to /root/cronicle-credentials.txt):

sudo cronicle-set-password

To choose your own (16 to 128 letters and digits), type it on standard input so it never appears on a command line:

sudo cronicle-set-password --stdin

Either way the account is unlocked if Cronicle had locked it after failed sign-ins. Signed in as admin, you can also change the password under My Account.

12

Users, privileges and jobs that need root

Add teammates under Admin, Users. Each account can be limited to running, creating, editing or deleting events, aborting jobs or toggling the scheduler, and to particular categories and server groups. Event parameters are checked against the plugin's own parameters, so a user who may create or run events cannot inject environment variables into a job (fixed in Cronicle 0.9.124).

Jobs run as the cronicle user. When a job needs root for a specific command, allow exactly that command with a sudoers rule rather than running Cronicle as root, for example:

echo 'cronicle ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp.service' | sudo tee /etc/sudoers.d/cronicle-myapp
sudo chmod 0440 /etc/sudoers.d/cronicle-myapp
sudo visudo -c
13

Maintenance

  • Operating system updates: unattended upgrades install Ubuntu security updates automatically. Reboot when /var/run/reboot-required exists.
  • Services: sudo systemctl status cronicle nginx. Cronicle's own logs are in /var/lib/cronicle/logs.
  • More servers: this image is a single Cronicle server (the master and the only worker). Cronicle can spread jobs over several servers, but the servers must be able to reach each other on Cronicle's port, which this image keeps on loopback. Plan the network and Cronicle's server_comm_use_hostnames, bind_address and udp_broadcast_port settings before you add servers.
  • Email notifications: set smtp_hostname, smtp_port and email_from in /opt/cronicle/conf/config.json, then sudo systemctl restart cronicle.

Support and licensing

Cronicle is open source under the MIT licence (the licence text is at /usr/share/doc/cronicle/LICENSE.md); there is no per-user or per-CPU licence fee. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support. For help with this image, email sales@secureanycloud.com.

Deploy on AWS

Launch Cronicle Job Scheduler 0.9.137 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