Ech0 5.8.2
Overview
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.
This guide covers the Ech0 5.8.2 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one software version of the SecureAnyCloud BookStack, CMS, Blogs and Wikis listing on AWS Marketplace, which you select when you subscribe and launch. Ech0 is a single Go program with an embedded SQLite database, so there is no database server to run.
What you get:
- 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 signs every session in its quick start 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. Ech0 stores only a bcrypt hash of the password. 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. The image adds an nftables firewall rule 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 it drops requests for a host name the instance does not serve.
- 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. The nginx access log never records query strings, because 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. Both services are blocked from the instance metadata service, and the AMI requires IMDSv2. - Current security fixes. Upstream builds Ech0 5.8.2 with Go 1.27.3 and current Go libraries, and
govulncheckfinds no reachable Go vulnerability in its binary. The binary is the unmodified upstream release, checked against its published checksum. - A fully patched base. Ubuntu 24.04 LTS is patched at build time, with unattended security upgrades switched on.
- Image contents. Ech0 5.8.2 as 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
Before you launch, make sure you have:
- 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. Limit port 22 to your own networks. Do not open 6277, because Ech0 is served on 443.
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, your key pair and a security group as described above. Ech0 is light, so a smaller instance is fine for a personal timeline.
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=ech0}]'Connect to your instance
Version SSH user
Ech0 5.8.2 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot takes a few seconds. When it finishes, the port 6277 firewall rule, the first-boot unit, Ech0 and nginx are all active:
systemctl is-active ech0-loopback-guard ech0-firstboot ech0 nginxactive active active activenginx serves ports 80 and 443. Ech0 listens on 6277 on every interface. The firewall rule drops every connection to 6277 that does not arrive on the instance's own loopback interface, and its 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 sac_ech0 inputState 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 sac_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/ech0Ech0 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/ech0Get your sign-in details
The credentials file holds the address Ech0 is served on (your instance's public DNS name by default), the user name, and the password generated on this instance. Read it with:
sudo cat /root/ech0-credentials.txtECH0_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ ECH0_ADMIN_USER=admin ECH0_ADMIN_PASSWORD=<24 letters and digits>Sign in
Open ECH0_URL in your browser. The certificate is self-signed and 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 the link only reports that registration is closed.
admin is the owner and administrator of your Ech0. After you sign in, the side menu adds Settings (the admin panel) and Publish.
Publish
Choose Publish and write your note in Markdown. The toolbar adds attachments and tags, and #word in the text is a tag too. Then choose +, and select Publish as public or Publish as private. Private posts are visible only to you when you are signed in.
Posts appear on the Feed, newest first. Status shows the posting heatmap and Tags lists your tags. The icons at the top link to the RSS feed and the other views.
Settings, users and registration
Settings opens the admin panel. Under Settings, System Settings you can set the site title and the Server URL that RSS and share links use. First boot sets the Server URL to https://<your instance's DNS name>. Allow registration 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.
Under Users, Account Settings changes your own user name, password and email. First boot sets the email to admin@localhost, so enter your own address here. User Manager lists and manages the other accounts.
The remaining sections cover storage, sign-in and data. Storage moves uploads to S3-compatible object storage. SSO adds OAuth2 or OpenID Connect sign-in. Data exports and imports. Logs shows Ech0's live log.
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/dataapp.log ech0.db ech0.db-shm ech0.db-wal files 1.1M /var/lib/ech0/dataEch0 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 ech0To 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.
Security checks
Run these checks to confirm the instance is locked down. 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/ || trueport 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: 000A result of 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. 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": 401Nobody 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")' doneinit/owner: refused register: refusedThe 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/sac-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 IPAddressDenyIPAddressDeny=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/128The 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 subjectAltNamesubject=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:localhostYour own DNS name and certificate
Point a DNS record at the instance, then set the name Ech0 is served on. This creates 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 certificate.
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 nginxOnce 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/sac-ech0. The image does not send one: browsers ignore HSTS from a site whose certificate they do not trust.
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 first start only, so change them in the web interface afterwards. Changing JWT_SECRET and then running 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 first. Download ech0-linux-amd64.tar.gz of the new release from the Ech0 releases page on GitHub and check its checksum. Replace /opt/ech0/ech0 with the binary inside (owned by root, mode 755), then restart Ech0 with sudo systemctl restart ech0. Your posts are untouched because they live in /var/lib/ech0/data.
Ech0 5.8.3 and later also honor the ECH0_SERVER_HOST=127.0.0.1 line that this image already has in /etc/ech0/ech0.env, and listen on loopback only. Keep the port 6277 firewall rule in place anyway. SecureAnyCloud also publishes new versions of this image as Ech0 releases appear.
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/sac/ech0-loopback.nft, ech0-loopback-guard.service
nginx site and proxy settings /etc/nginx/sites-available/sac-ech0, /etc/nginx/snippets/sac-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)
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 SecureAnyCloud's hardening, first boot setup and support.
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
- Support: 24/7, with a 24 hour response SLA
Deploy on AWS
Launch Ech0 5.8.2 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