Ech0 5.8.2 on AWS User Guide
Overview
This guide covers launching and using the Ech0 5.8.2 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg BookStack, CMS, Blogs and Wikis listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Ech0 is an open source, self hosted micro blog: a fast, minimal place to post short notes, links, images and audio to a timeline you run yourself. Posts are written in Markdown and can be public or private, tagged and searched; the timeline has an RSS feed, a posting heatmap and a documented REST API with personal access tokens. Ech0 is one Go program with an embedded SQLite database, so there is no database server to run.
The cloudimg image adds:
- No open setup and no default login. Out of the box Ech0 makes whoever first opens its setup page the owner and administrator, leaves self registration open, and its quick start signs every session with the published secret
Hello Echos. On this image your instance's first boot creates the owneradminwith a password generated on the instance, generates the instance's own session signing secret, and turns self registration off, all before the web server starts. Only a bcrypt hash of the password is stored by Ech0; the password itself is in/root/ech0-credentials.txt(readable by root only). Nothing from the image build ships in the image. - HTTPS from the first boot. nginx serves Ech0 on port 443 with a certificate generated on your instance's first boot, and port 80 redirects to HTTPS. Ech0's sign-in cookie is marked
Secure. - Ech0 is never exposed directly. Ech0 5.8.2 always listens on port 6277 on every network interface, so the image adds a firewall rule (nftables) that drops every connection to port 6277 that does not come from the instance itself, and Ech0 will not start without that rule. nginx is the only public listener, and requests for a host name the instance does not serve are dropped.
- The real client address. Ech0 keys its rate limits and like counting on the client address. nginx always replaces any
X-Forwarded-FororX-Real-IPheader a client sends with the connection's own address, so a client cannot fake its address. Ech0 has no sign-in rate limit of its own, so nginx limits how fast one address can call the sign-in, registration and token endpoints and answers429 Too Many Requeststo a flood. nginx's access log never records query strings (Ech0's live log viewer sends its sign-in token in the URL). - Uploads cannot run in the site. Ech0 accepts only image, audio and video types and sends
nosniff; nginx adds a sandboxing content security policy to every uploaded file it serves. - Fail closed and confined. Ech0 runs as its own unprivileged user with no Linux capabilities and can write only its data folder (
/var/lib/ech0). nginx refuses to start until first boot has finished, and both services are blocked from the instance metadata service; the AMI requires IMDSv2. - Current security fixes. Ech0 5.8.2 is built by upstream with Go 1.27.3 and current Go libraries;
govulncheckfinds no reachable Go vulnerability in its binary. The binary is the unmodified upstream release, checked against its published checksum. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Ech0 5.8.2 (one self contained binary, /opt/ech0/ech0, with its web interface built in), nginx, nftables, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The timeline starts empty.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
- A security group that allows TCP 22 (SSH), TCP 443 (Ech0) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep 22 to your own networks. Do not open 6277: Ech0 is served on 443.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Ech0 5.8.2 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (Ech0 is light; a smaller instance is fine for a personal timeline), your key pair and a security group as above.
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=ech0}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Ech0 5.8.2 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes a few seconds. Then the port 6277 firewall rule, the first boot unit, Ech0 and nginx are active:
systemctl is-active ech0-loopback-guard ech0-firstboot ech0 nginx
active
active
active
active
nginx serves ports 80 and 443. Ech0 listens on 6277 on every interface, and the firewall rule below drops every connection to 6277 that does not arrive on the instance's own loopback interface (the counter shows how many were dropped). There is no database port:
sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|ech0/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
sudo nft list chain inet cloudimg_ech0 input
State Local Peer
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN *:6277 users:(("ech0"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))
table inet cloudimg_ech0 {
chain input {
type filter hook input priority filter - 10; policy accept;
iif "lo" accept
tcp dport 6277 counter packets <dropped> bytes <bytes> drop
}
}
The release record names the upstream binary and the source it comes from:
cat /usr/share/doc/ech0/RELEASE
sha256sum /opt/ech0/ech0
Ech0 5.8.2
release asset sha256 9749e0c29984eaa59b337d5da9fd590c4ef7910350f4358041141e0700c7b460
binary sha256 36e3ffcebac708934077699c08123ad267b7d5ce6cddc7f668e8e4a93d2ef195
source sha256 e41ba97c68593241fdb50681df9ff21d2d536bd7f893a6268eb9b2a72234754b
commit 47d8d345c3660a0a4562c9a589d58aa645abdf65
license AGPL-3.0-or-later sha256 0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0
36e3ffcebac708934077699c08123ad267b7d5ce6cddc7f668e8e4a93d2ef195 /opt/ech0/ech0
Step 4: Get your sign-in details
The credentials file holds the address Ech0 is served as (your instance's public DNS name by default), the user name and the password generated on this instance. Show it with:
sudo cat /root/ech0-credentials.txt
ECH0_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
ECH0_ADMIN_USER=admin
ECH0_ADMIN_PASSWORD=<24 letters and digits>
Step 5: Sign in
Open ECH0_URL in your browser. The certificate is self signed (generated on your instance), so accept the browser warning once, or install your own certificate (Step 10). The timeline is public; to sign in, open /auth (or choose the sign-in icon at the top of the timeline) and sign in as admin with ECH0_ADMIN_PASSWORD. The page also shows a Register link: registration is turned off on this image, so it only answers that registration is closed.

admin is the owner and administrator of your Ech0. Signed in, the side menu adds Settings (the admin panel) and Publish.
Step 6: Publish
Choose Publish, write your note in Markdown (the toolbar adds attachments and tags; #word in the text is a tag too), then choose + and Publish as public or Publish as private. Private posts are visible only to you when signed in.

Posts appear on the Feed, newest first. Status shows the posting heatmap, Tags lists your tags, and the icons at the top link the RSS feed and the other views.

Step 7: Settings, users and registration
Settings opens the admin panel. Settings, System Settings holds the site title, the Server URL that RSS and share links use (set on first boot to https://<your instance's DNS name>) and Allow registration, which is off. Turn it on only if you want other people to create their own accounts. Access Tokens creates personal access tokens for the REST API.

Users, Account Settings changes your own user name, password and email (first boot set the email to admin@localhost; put your own address here). User Manager lists and manages the other accounts. Storage moves uploads to S3 compatible object storage, SSO adds OAuth2 or OpenID Connect sign-in, Data exports and imports, and Logs shows Ech0's live log.

Step 8: Where your data lives
Everything Ech0 stores is under /var/lib/ech0/data: the SQLite database (ech0.db with its -wal and -shm files), uploaded files (files/) and Ech0's own log (app.log). The session signing secret and Ech0's settings from first boot are in /etc/ech0/ech0.env (readable by root only):
sudo ls /var/lib/ech0/data
sudo du -sh /var/lib/ech0/data
app.log
ech0.db
ech0.db-shm
ech0.db-wal
files
1.1M /var/lib/ech0/data
Ech0 has an export and import under Settings, Data. For a full backup, stop Ech0 so the database is consistent, copy the data folder and the environment file, and start it again:
sudo systemctl stop ech0
sudo tar czf /home/ubuntu/ech0-backup-$(date +%F).tar.gz -C / var/lib/ech0/data etc/ech0/ech0.env
sudo systemctl start ech0
To keep the data on its own EBS volume, attach the volume, stop Ech0, copy /var/lib/ech0 onto the volume, mount the volume on /var/lib/ech0 (by UUID in /etc/fstab), make sure it is owned by ech0:ech0 with mode 750, and start Ech0 again. EBS snapshots of that volume are then your backups.
Step 9: Security checks
Port 80 sends browsers to HTTPS, Ech0's health endpoint answers through nginx, the owner already exists, and requests for a host name the instance does not serve are dropped:
H=$(sed -n 's/^set \$ech0_host "\(.*\)";$/\1/p' /etc/nginx/ech0-host.conf)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz" | python3 -c 'import json,sys; print("health:", json.load(sys.stdin)["data"])'
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/api/init/status" | python3 -c 'import json,sys; print("setup:", json.load(sys.stdin)["data"])'
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/
health: {'status': 'ok', 'version': '5.8.2'}
setup: {'initialized': True, 'owner_exists': True}
unknown host name: 000
(000 means nginx closed the connection without an answer.) The upstream style default admin / admin is refused, the password generated on this instance signs in, and a session token signed with the upstream default secret Hello Echos is rejected. This block hands the password and the token to curl on standard input, never on a command line, and prints neither:
H=$(sed -n 's/^set \$ech0_host "\(.*\)";$/\1/p' /etc/nginx/ech0-host.conf)
R="--resolve $H:443:127.0.0.1"
curl -sk $R -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin"}' "https://$H/api/login" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print("admin/admin: token issued" if (d.get("data") or {}).get("access_token") else "admin/admin: refused")'
T=$(sudo sed -n 's/^ECH0_ADMIN_PASSWORD=\(.*\)/{"username":"admin","password":"\1"}/p' /root/ech0-credentials.txt \
| curl -sk $R -H 'Content-Type: application/json' --data-binary @- "https://$H/api/login" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["access_token"])')
printf 'header = "Authorization: Bearer %s"\n' "$T" | curl -sk $R -K - -o /dev/null -w 'generated password, signed-in request: %{http_code}\n' "https://$H/api/user"
HP=$(printf %s "$T" | cut -d. -f1,2)
SIG=$(printf %s "$HP" | openssl dgst -binary -sha256 -hmac 'Hello Echos' | openssl base64 -A | tr '+/' '-_' | tr -d '=')
printf 'header = "Authorization: Bearer %s.%s"\n' "$HP" "$SIG" | curl -sk $R -K - -o /dev/null -w 'token signed with "Hello Echos": %{http_code}\n' "https://$H/api/user"
admin/admin: refused
generated password, signed-in request: 200
token signed with "Hello Echos": 401
Nobody can take over the setup or register an account:
H=$(sed -n 's/^set \$ech0_host "\(.*\)";$/\1/p' /etc/nginx/ech0-host.conf)
for u in init/owner register; do
curl -sk --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' \
-d '{"username":"stranger","password":"StrangerPass123","email":"stranger@example.com"}' "https://$H/api/$u" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print("'$u':", "refused" if d.get("code") == 0 else "ACCEPTED")'
done
init/owner: refused
register: refused
The credentials and environment files are readable by root only, the data folder by Ech0's own user only, and the binary belongs to root. Registration is seeded off, the server URL is the instance's own name, and the owner's password is stored as a bcrypt hash:
sudo stat -c '%a %U:%G %n' /root/ech0-credentials.txt /etc/ech0/ech0.env /var/lib/ech0/data /var/lib/ech0/data/ech0.db /opt/ech0/ech0
sudo grep -E '^ECH0_' /etc/ech0/ech0.env | sed -E 's#(SERVER_URL=https://).*#\1<your-host>#'
sudo python3 -c 'import sqlite3; c=sqlite3.connect("file:/var/lib/ech0/data/ech0.db?mode=ro", uri=True); [print(r[0], r[1], r[2][:7]) for r in c.execute("select u.username, a.password_algo, a.password_hash from users u join user_local_auth a on a.user_id = u.id")]'
600 root:root /root/ech0-credentials.txt
600 root:root /etc/ech0/ech0.env
750 ech0:ech0 /var/lib/ech0/data
640 ech0:ech0 /var/lib/ech0/data/ech0.db
755 root:root /opt/ech0/ech0
ECH0_SERVER_PORT=6277
ECH0_SERVER_HOST=127.0.0.1
ECH0_SETTING_ALLOW_REGISTER=false
ECH0_SETTING_SERVER_URL=https://<your-host>
ECH0_LOG_CONSOLE=false
admin bcrypt $2a$10$
nginx replaces the client address headers with the connection's own address and passes the configured host name, never the one a client sends:
grep -E 'X-Forwarded-For|X-Real-IP|Host ' /etc/nginx/snippets/cloudimg-ech0-proxy.conf | grep -v '^#'
proxy_set_header Host $ech0_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host "";
Ech0 runs as ech0 with no capabilities, a read-only system and only its data folder writable, it cannot start without the port 6277 firewall rule, and neither service can reach the instance metadata service:
systemctl show ech0 -p User -p ProtectSystem -p ReadWritePaths -p NoNewPrivileges -p Requires -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
User=ech0
ReadWritePaths=/var/lib/ech0
ProtectSystem=strict
NoNewPrivileges=yes
Requires=system.slice -.mount ech0-loopback-guard.service sysinit.target
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
The certificate was generated on this instance and names its own addresses:
H=$(sed -n 's/^set \$ech0_host "\(.*\)";$/\1/p' /etc/nginx/ech0-host.conf)
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 = cloudimg
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
Step 10: Your own DNS name and certificate
Point a DNS record at the instance, then set the name Ech0 is served as. This mints a new self signed certificate for the name, updates nginx and the URL in /root/ech0-credentials.txt, and keeps the name across reboots:
sudo ech0-set-hostname <your-domain>
Then set Settings, System Settings, Server URL to https://<your-domain> so RSS and share links use it.
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running ech0-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/ech0.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/ech0.key
sudo nginx -t && sudo systemctl reload nginx
Once browsers trust the certificate you can also add an HSTS header (add_header Strict-Transport-Security "max-age=31536000" always;) to the port 443 server in /etc/nginx/sites-available/cloudimg-ech0. The image does not send one: browsers ignore HSTS from a site whose certificate they do not trust.
Step 11: Passwords, settings and upgrades
Change the administrator password in the web interface (Settings, Users, Account Settings). The password in /root/ech0-credentials.txt is the one first boot generated; it is not updated when you change the password in Ech0.
/etc/ech0/ech0.env holds the session signing secret (JWT_SECRET) and the settings Ech0 used for its first start. Ech0 copies settings such as registration and the server URL into its database on its first start only, so change them in the web interface afterwards. Changing JWT_SECRET (then sudo systemctl restart ech0) signs every session out; keep a copy of it with your backups.
To upgrade to a later Ech0 release, snapshot the instance, download ech0-linux-amd64.tar.gz of the new release from the Ech0 releases page on GitHub and check its checksum, then replace /opt/ech0/ech0 with the binary inside (owned by root, mode 755) and restart Ech0 (sudo systemctl restart ech0). Your posts are untouched because they live in /var/lib/ech0/data. Ech0 5.8.3 and later also honour the ECH0_SERVER_HOST=127.0.0.1 line this image already has in /etc/ech0/ech0.env and listen on loopback only; keep the port 6277 firewall rule in place anyway. cloudimg also publishes new versions of this image as Ech0 releases appear.
Step 12: Server components
| Component | Path |
|---|---|
| Ech0 binary (root owned) | /opt/ech0/ech0 |
| Ech0 data (database, uploads, log) | /var/lib/ech0/data |
| Session signing secret and first start settings | /etc/ech0/ech0.env (mode 0600) |
| Ech0 service | /etc/systemd/system/ech0.service |
| Port 6277 firewall rule | /etc/cloudimg/ech0-loopback.nft, ech0-loopback-guard.service |
| nginx site and proxy settings | /etc/nginx/sites-available/cloudimg-ech0, /etc/nginx/snippets/cloudimg-ech0-proxy.conf |
| Certificate and key | /etc/nginx/ssl/ |
| First boot script and hostname helper | /usr/local/sbin/ech0-firstboot.sh, /usr/local/sbin/ech0-set-hostname |
| Licence, source and release record | /usr/share/doc/ech0/ |
| Credentials | /root/ech0-credentials.txt (mode 0600) |
Step 13: Security recommendations
- Allow port 22 in the security group from your own networks only, and port 443 from the networks that should reach Ech0. Never open port 6277.
- Keep registration off unless you want other people to create accounts, and give API access tokens the smallest scopes they need.
- Use your own DNS name and a certificate your browsers trust (Step 10).
- Settings, System Settings accepts custom CSS and JavaScript that every visitor's browser runs; keep them to content you trust.
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else. - Back up
/var/lib/ech0/dataand/etc/ech0/ech0.env(Step 8).
Support and licensing
Ech0 is open source under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later); there is no per user or per CPU licence fee. The image ships the unmodified upstream 5.8.2 release binary; its licence, notice and the exact upstream source archive (the Corresponding Source) are in /usr/share/doc/ech0/. If you modify Ech0 and let other people use it over a network, the AGPL asks you to offer them the source of your version. The AMI price covers cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA