Streaming & Messaging AWS

Liftbridge 26.01.1 Message Streams on AWS User Guide

| Product: Liftbridge 26.01.1 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Liftbridge 26.01.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg streaming and messaging listing on AWS Marketplace; you pick it as the software version when you subscribe.

Liftbridge is an open source (Apache-2.0) server for durable message streams, written in Go. A stream is an append-only log attached to a NATS subject: clients publish messages to it, every message gets an offset (0, 1, 2 and so on), and clients subscribe from any point in the log (the earliest message, a given offset, the latest, or only new messages). Messages stay on disk until the stream's retention removes them, so a consumer that was offline catches up from where it left off. Clients talk to Liftbridge's gRPC API with a Liftbridge client library (go-liftbridge, and community clients for Python, Java and Node.js) or with the lift-cli tool on the image.

What the cloudimg image adds:

  • gRPC with mutual TLS only, on 9292. Clients reach Liftbridge on 9292 over TLS 1.3 or 1.2, and every client must present a client certificate issued by this instance's own certificate authority. A client without a certificate, with a certificate from another CA, with a self-signed certificate, in clear text (h2c) or on TLS 1.0 or 1.1 is refused (Step 9). Nothing else answers on the network (Step 10).
  • A certificate authority made on your instance. At first boot the instance creates its own CA, a server certificate for its EC2 names and addresses, and one client certificate, liftbridge-client-1. Nothing comes from the image, so no two instances share a key. liftbridge-client-cert issues more client certificates and can replace the CA to revoke them all (Step 8).
  • The embedded NATS server is private. Liftbridge runs its own NATS server, which it uses internally. Here it listens on the loopback address 127.0.0.1:4222 only, requires a login generated at first boot, and only the liftbridge service user and root may connect to it. It has no monitoring, cluster, gateway, leaf-node, WebSocket or MQTT port (Step 10).
  • A server that refuses to run unsafely. Before every start a configuration gate refuses an unsafe change (clear-text gRPC, no client certificates, telemetry on, NATS on a network address or without its login, a NATS monitoring or cluster port, a key that does not match its certificate, a configuration readable by others). After every start a guard checks the running service and stops it if anything is wrong. If first boot cannot create the CA, the certificates or the NATS login, Liftbridge never starts (Step 12).
  • No telemetry, no instance metadata, no outbound network. Liftbridge's telemetry (on by default upstream) is off. The service runs with the EC2 instance metadata address blocked, and a firewall table lets the liftbridge user answer its clients and reach its own NATS server on 127.0.0.1:4222, and nothing else, in IPv4 and IPv6 (Step 13).
  • A cloudimg build from the v26.01.1 tag on a current Go. The upstream 26.01.1 release binary was built with Go 1.25.6 and modules that have known vulnerabilities. cloudimg builds liftbridge from the unmodified v26.01.1 tag source with Go 1.27.3, raising four modules to fixed versions, and records how you can rebuild it byte for byte (Step 15). liftbridge --version reports 26.01.1. Ubuntu 24.04 LTS is fully patched at build time.

Two things to know before you start. Liftbridge has no per-stream access rules: any client with a valid certificate may create, publish to, read and delete every stream. Give each application its own certificate so you know who connects, and use a separate instance for parties that must not see each other's streams. And Liftbridge has no certificate revocation list: to cut off one lost client key you replace the CA, which stops every client certificate at once (Step 8).

What is on the image: Liftbridge 26.01.1 with its embedded NATS server (/usr/local/bin/liftbridge; configuration /etc/liftbridge/liftbridge.yaml and /etc/liftbridge/nats.conf; certificates in /etc/liftbridge/tls/; data in /var/lib/liftbridge), the lift-cli client (/usr/local/bin/lift-cli), and the cloudimg helpers liftbridge-selfcheck, liftbridge-client-cert, liftbridge-set-hostname, liftbridge-install-cert, liftbridge-config-gate, liftbridge-guard and liftbridge-probe.

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:
Port Protocol Source Purpose
22 TCP your admin network SSH
9292 TCP the networks your Liftbridge clients connect from Liftbridge gRPC with mutual TLS

Nothing else needs to be open. NATS (4222), NATS monitoring (8222) and NATS clustering (6222) are not served to the network, so opening them achieves nothing.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Liftbridge 26.01.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (or larger for high message rates), your key pair and a security group as above. Streams are stored on the root volume, so give it room for the data you keep (Step 11).

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 \
  --block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=50,VolumeType=gp3}' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=liftbridge}]'

The image requires IMDSv2 (session tokens). First boot takes under a minute after the instance is running: a boot service reads the instance's EC2 names, the first-boot service creates the CA, the certificates and the NATS login, then Liftbridge starts and the guard checks it.

Step 2: Connect to your instance

Version SSH user
Liftbridge 26.01.1 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

The login banner shows where the endpoint and the certificates are:

  Liftbridge 26.01.1 on Ubuntu 24.04 (cloudimg)
  Endpoint, NATS login, fingerprints:  sudo cat /root/liftbridge-credentials.txt
  Clients: gRPC with mutual TLS on <this host>:9292; CA + client certificate in /root/liftbridge-client/
  Server:  systemctl status liftbridge   Verify: sudo liftbridge-selfcheck   Certs: sudo liftbridge-client-cert list
  User guide: https://www.cloudimg.co.uk/guides/liftbridge-26-01-1-on-ubuntu-24-04-aws/

Step 3: Check the services and the listening set

systemctl is-active liftbridge-metadata-names liftbridge-firstboot liftbridge-egress-guard liftbridge
sudo ss -tlnp | grep -E ':(22|4222|6222|8222|9292) '
sudo ss -ulnp | grep -c liftbridge || true
active
active
active
active
LISTEN 0      4096       127.0.0.1:4222      0.0.0.0:*    users:(("liftbridge",pid=17412,fd=3))
LISTEN 0      4096         0.0.0.0:22        0.0.0.0:*    users:(("sshd",pid=822,fd=3),("systemd",pid=1,fd=177))
LISTEN 0      4096            [::]:22           [::]:*    users:(("sshd",pid=822,fd=4),("systemd",pid=1,fd=181))
LISTEN 0      4096               *:9292            *:*    users:(("liftbridge",pid=17412,fd=16))
0

liftbridge-metadata-names reads this instance's EC2 names for the server certificate, liftbridge-firstboot prepares the CA, the certificates and the configuration, liftbridge-egress-guard loads the firewall table, and liftbridge is the server. Liftbridge listens on 9292 on every address (IPv4 and IPv6) for clients, and its embedded NATS server on 127.0.0.1:4222 only. There is no UDP socket.

Step 4: Your endpoint and the credentials files

First boot writes everything a client needs to two places, both readable by root only:

  • /root/liftbridge-credentials.txt (0600): the endpoint, the certificate fingerprints, the paths of the client files and the NATS login.
  • /root/liftbridge-client/ (0700): ca.crt (this instance's CA certificate), liftbridge-client-1.crt and liftbridge-client-1.key (the first client certificate and its key), and any client certificate you add later.

Show the endpoint and the fingerprints (this leaves out the NATS password, which no client needs):

sudo grep -E '^LIFTBRIDGE_(HOST|ADDR|PORT|PRIVATE_IP|TLS_CA_FILE|TLS_CA_SHA256|CLIENT_CERT_FILE|CLIENT_KEY_FILE|SERVER_CERT_SHA256)=' /root/liftbridge-credentials.txt
LIFTBRIDGE_HOST=ec2-3-90-50-227.compute-1.amazonaws.com
LIFTBRIDGE_ADDR=ec2-3-90-50-227.compute-1.amazonaws.com:9292
LIFTBRIDGE_PORT=9292
LIFTBRIDGE_PRIVATE_IP=172.31.95.40
LIFTBRIDGE_TLS_CA_FILE=/root/liftbridge-client/ca.crt
LIFTBRIDGE_TLS_CA_SHA256=<the CA certificate's SHA-256 fingerprint>
LIFTBRIDGE_CLIENT_CERT_FILE=/root/liftbridge-client/liftbridge-client-1.crt
LIFTBRIDGE_CLIENT_KEY_FILE=/root/liftbridge-client/liftbridge-client-1.key
LIFTBRIDGE_SERVER_CERT_SHA256=<the server certificate's SHA-256 fingerprint>

LIFTBRIDGE_ADDR is the address Liftbridge advertises: Liftbridge clients connect to the address you give them, ask the server for its metadata, then connect to the advertised address, so it must be a name your clients can reach. It is this instance's EC2 public DNS name (or, without one, its public IPv4, private DNS name or private IPv4). Inside the VPC the public DNS name resolves to the private address. To advertise your own DNS name, see Step 14.

The server certificate covers all of these names and addresses plus 127.0.0.1 and localhost, and is issued by this instance's CA. Clients pin ca.crt for this server only: give it to the client as its CA file (-ca in lift-cli, RootCAs in Go). Never add it to a system or browser trust store.

Step 5: Test on the instance

liftbridge-selfcheck runs the guard and a full round trip over mutual TLS with liftbridge-client-1: it creates a stream, publishes 3 messages, reads them back from offset 0 and deletes the stream.

sudo liftbridge-selfcheck
liftbridge-guard: OK (check): listeners tcp 0.0.0.0:9292 and 127.0.0.1:4222 only; 9292 refuses a client without a certificate and clear-text h2c; NATS refuses a client without the login; uid liftbridge egress replies and 127.0.0.1:4222 only
liftbridge-selfcheck: round trip over mutual TLS to ec2-3-90-50-227.compute-1.amazonaws.com:9292: stream created, 3 messages published and read back from offset 0, stream deleted
SELFCHECK OK

The same by hand with lift-cli (as root, because the client key is root-only):

C=/root/liftbridge-client; ADDR=$(sudo sed -n 's/^LIFTBRIDGE_ADDR=//p' /root/liftbridge-credentials.txt)
LC=(sudo lift-cli); TLS=(-addr "$ADDR" -ca "$C/ca.crt" -cert "$C/liftbridge-client-1.crt" -key "$C/liftbridge-client-1.key")
"${LC[@]}" create "${TLS[@]}" -stream orders -subject orders.events
for i in 1 2 3; do "${LC[@]}" publish "${TLS[@]}" -stream orders -message "order $i placed"; done
"${LC[@]}" subscribe "${TLS[@]}" -stream orders -count 3 -start earliest
"${LC[@]}" delete "${TLS[@]}" -stream orders
OK created stream "orders" (subject "orders.events")
OK published to "orders" offset=0
OK published to "orders" offset=1
OK published to "orders" offset=2
offset=0 value=order 1 placed
offset=1 value=order 2 placed
offset=2 value=order 3 placed
OK deleted stream "orders"

Step 6: Copy the client certificate, key and CA to a client

A client needs three files: ca.crt, a client certificate and its key. On the instance, put copies in a private archive you can fetch with scp:

sudo tar -C /root/liftbridge-client -czf /home/ubuntu/liftbridge-client-1.tgz ca.crt liftbridge-client-1.crt liftbridge-client-1.key
sudo chown ubuntu:ubuntu /home/ubuntu/liftbridge-client-1.tgz && chmod 0600 /home/ubuntu/liftbridge-client-1.tgz
ls -l /home/ubuntu/liftbridge-client-1.tgz

On the client machine, fetch and unpack it into a private directory, and copy lift-cli too if the client is a Linux x86_64 machine (it is a static binary with no dependencies):

mkdir -m 0700 -p ~/liftbridge && cd ~/liftbridge
scp -i <your-key.pem> ubuntu@<public-ip>:liftbridge-client-1.tgz . && tar -xzf liftbridge-client-1.tgz && rm liftbridge-client-1.tgz
chmod 0600 liftbridge-client-1.key
scp -i <your-key.pem> ubuntu@<public-ip>:/usr/local/bin/lift-cli . && chmod 0755 lift-cli

Then remove the archive from the instance (the originals stay in /root/liftbridge-client/):

shred -u /home/ubuntu/liftbridge-client-1.tgz && echo removed

Treat the .key file like a password: anyone who has it and can reach port 9292 can read and write every stream.

Step 7: Connect a client from another machine

Allow 9292 from the client's network in the security group, then, in ~/liftbridge on the client, using LIFTBRIDGE_ADDR from Step 4:

TLS="-addr <liftbridge-addr> -ca ca.crt -cert liftbridge-client-1.crt -key liftbridge-client-1.key"
./lift-cli metadata $TLS
./lift-cli create $TLS -stream orders -subject orders.events
for i in 1 2 3 4 5; do ./lift-cli publish $TLS -stream orders -message "order $i placed"; done
./lift-cli subscribe $TLS -stream orders -count 5 -start earliest
OK connected — 1 broker(s) known
OK created stream "orders" (subject "orders.events")
OK published to "orders" offset=0
...
offset=0 value=order 1 placed
offset=1 value=order 2 placed
offset=2 value=order 3 placed
offset=3 value=order 4 placed
offset=4 value=order 5 placed

The messages are durable: restart Liftbridge (sudo systemctl restart liftbridge) or reboot the instance and subscribe -start earliest reads the same offsets again.

In your own Go code, go-liftbridge v2 takes the same three files. A complete program that creates a stream with a 24-hour retention, publishes a message and reads the stream from offset 0:

mkdir liftbridge-demo && cd liftbridge-demo && go mod init example.com/liftbridge-demo
go get github.com/liftbridge-io/go-liftbridge/v2@v2.4.0
cat > main.go <<'EOF'
package main

import (
    "context"
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "os"
    "time"

    lift "github.com/liftbridge-io/go-liftbridge/v2"
)

func main() {
    addr, dir := os.Args[1], os.Args[2] // <liftbridge-addr> and the directory with the three files
    cert, err := tls.LoadX509KeyPair(dir+"/liftbridge-client-1.crt", dir+"/liftbridge-client-1.key")
    if err != nil {
        panic(err)
    }
    caPEM, err := os.ReadFile(dir + "/ca.crt")
    if err != nil {
        panic(err)
    }
    pool := x509.NewCertPool()
    pool.AppendCertsFromPEM(caPEM) // trust this instance's CA only, for this connection only
    client, err := lift.Connect([]string{addr}, lift.TLSConfig(&tls.Config{
        Certificates: []tls.Certificate{cert}, RootCAs: pool, MinVersion: tls.VersionTLS12}))
    if err != nil {
        panic(err)
    }
    defer client.Close()
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    err = client.CreateStream(ctx, "events", "events", lift.RetentionMaxAge(24*time.Hour))
    if err != nil && err != lift.ErrStreamExists {
        panic(err)
    }
    if _, err := client.Publish(ctx, "events", []byte("hello from go-liftbridge")); err != nil {
        panic(err)
    }
    done := make(chan struct{})
    err = client.Subscribe(ctx, "events", func(msg *lift.Message, err error) {
        if err == nil {
            fmt.Printf("offset=%d value=%s\n", msg.Offset(), msg.Value())
            close(done)
        }
    }, lift.StartAtEarliestReceived())
    if err != nil {
        panic(err)
    }
    <-done
}
EOF
go run . <liftbridge-addr> ~/liftbridge

Other client libraries take the same three files as their CA, certificate and key options.

Step 8: Issue more client certificates, and revoke them

Give each application its own client certificate:

sudo liftbridge-client-cert add app-orders
sudo liftbridge-client-cert list
sudo ls /root/liftbridge-client/
liftbridge-client-cert: issued app-orders: /root/liftbridge-client/app-orders.crt and /root/liftbridge-client/app-orders.key (copy them and /root/liftbridge-client/ca.crt to the client)
NAME                             SERIAL                               EXPIRES
liftbridge-client-1              8AF3DD0549C7ACF0C4879F193C798E58     Feb 10 16:28:31 2029 GMT
app-orders                       B103BBEF8EC8056570E4822FD4023303     Feb 10 16:40:04 2029 GMT
app-orders.crt
app-orders.key
ca.crt
liftbridge-client-1.crt
liftbridge-client-1.key

A name is 1 to 63 lower-case letters, digits or -. Each certificate is an EC P-256 key with a certificate valid for 825 days that may only be used by a client (it cannot act as a server or sign other certificates). Copy <name>.crt, <name>.key and ca.crt to that client as in Step 6.

Why the CA key stays on the instance. liftbridge-client-cert needs the CA key to issue certificates, and the boot service needs it to re-issue the server certificate when the instance's names change (an EC2 stop and start gives it a new public name). The key is kept in /etc/cloudimg/liftbridge/ (root only, 0700, the key file 0600). It never leaves the instance and is never readable by the liftbridge user, and anyone who could read it is already root on this server.

Revoking. Liftbridge does not check a revocation list, so a certificate stays valid until it expires. To cut off a lost or leaked client key, replace the CA:

sudo liftbridge-client-cert new-ca

This makes a new CA, a new server certificate (same server key) and a new liftbridge-client-1, and restarts Liftbridge. Every earlier client certificate stops working at once. Copy the new ca.crt and a new client certificate to every client you keep, and add the others again with add.

Step 9: Mutual TLS is enforced

With a client certificate the TLS handshake completes on TLS 1.3. Without one, the server ends the handshake, and TLS 1.0 and 1.1 are refused:

C=/root/liftbridge-client; H=$(sudo sed -n 's/^LIFTBRIDGE_HOST=//p' /root/liftbridge-credentials.txt)
sudo openssl s_client -connect "$H:9292" -servername "$H" -CAfile $C/ca.crt -cert $C/liftbridge-client-1.crt -key $C/liftbridge-client-1.key -brief </dev/null 2>&1 | grep -E 'Protocol version|Verification' || true
sudo openssl s_client -connect "$H:9292" -servername "$H" -CAfile $C/ca.crt -brief </dev/null 2>&1 | grep -o 'alert certificate required' | head -1 || true
sudo openssl s_client -connect "$H:9292" -servername "$H" -CAfile $C/ca.crt -cert $C/liftbridge-client-1.crt -key $C/liftbridge-client-1.key -tls1_1 -cipher 'DEFAULT@SECLEVEL=0' -brief </dev/null 2>&1 | grep -o 'alert protocol version' | head -1 || true
liftbridge-probe h2c --host "$H" --port 9292
Protocol version: TLSv1.3
Verification: OK
alert certificate required
alert protocol version
NOT-H2C eof

NOT-H2C means a clear-text gRPC (HTTP/2) client got no answer. A client certificate issued by another instance's CA, or a self-signed one, is refused the same way as no certificate.

Step 10: Why NATS 4222 is loopback-only and authenticated

Liftbridge stores its streams through its embedded NATS server: every message published to a stream's subject passes through NATS, and Liftbridge itself is a NATS client. Upstream's default lets NATS listen without a login, so any process that can reach 4222 could read and write the messages behind every stream and bypass Liftbridge's mutual TLS. On this image:

  • NATS listens on 127.0.0.1:4222 only, so nothing outside the instance can reach it, whatever the security group says.
  • It requires the login liftbridge with a password generated at first boot (LIFTBRIDGE_NATS_* in the credentials file, and in the root-only Liftbridge configuration).
  • A firewall rule refuses connections to 4222 from every local user except the liftbridge service user and root, so another account on the instance cannot even try the password.
  • It has no monitoring (8222), cluster (6222), gateway, leaf-node, WebSocket or MQTT port, and no JetStream.
IP=$(sudo sed -n 's/^LIFTBRIDGE_PRIVATE_IP=//p' /root/liftbridge-credentials.txt)
for p in 4222 6222 8222 9292; do nc -z -w 3 "$IP" "$p" 2>/dev/null && echo "$IP:$p open" || echo "$IP:$p closed"; done
nc -z -w 3 127.0.0.1 4222 && echo "ubuntu -> 127.0.0.1:4222 open" || echo "ubuntu -> 127.0.0.1:4222 refused"
sudo liftbridge-probe nats --host 127.0.0.1 --port 4222
172.31.95.40:4222 closed
172.31.95.40:6222 closed
172.31.95.40:8222 closed
172.31.95.40:9292 open
ubuntu -> 127.0.0.1:4222 refused
NATS-REFUSED Authorization Violation auth_required=True

The last line is root connecting without the login: NATS refuses it.

Step 11: The data directory and retention

Streams are stored in /var/lib/liftbridge (owned by the liftbridge user, mode 0700), on the root volume. It is the only directory the service may write.

Liftbridge's defaults apply to every stream unless the client sets its own values when it creates the stream:

Setting Default on this image Per-stream option in go-liftbridge
Retention by age 7 days lift.RetentionMaxAge(d)
Retention by size none (no limit) lift.RetentionMaxBytes(n)
Retention by message count none (no limit) lift.RetentionMaxMessages(n)
Segment size 256 MiB lift.SegmentMaxBytes(n)

Retention removes whole closed segments, so a stream can briefly hold a little more than its limit. The configuration gate accepts only the shipped server configuration, so these server-wide defaults are fixed; set the retention you need on each stream when you create it (Step 7 shows RetentionMaxAge). A stream deleted with lift-cli delete (or DeleteStream) frees its space at once.

Watch the space with df -h /var/lib/liftbridge. To make room, grow the EBS root volume (modify the volume in EC2, then sudo growpart /dev/nvme0n1 1 && sudo resize2fs /dev/root) or delete or shorten streams. For a backup, take an EBS snapshot of the root volume.

Step 12: The server refuses to run unsafely

  • Before every start, liftbridge-config-gate (run by systemd as root before Liftbridge) compares the configuration with the shipped template and refuses clear-text gRPC, a configuration without client certificates, telemetry on, NATS on a network address or without its login, a NATS monitoring or cluster port, a server key that does not match its certificate, a server certificate from another CA, no client CA, a configuration readable by others and a data directory others can open. A refused start leaves nothing listening.
  • After every start, liftbridge-guard checks the listening set (9292 and 127.0.0.1:4222 only), that 9292 refuses a client without a certificate and clear text, that NATS refuses a client without the login, and that the firewall table is loaded. If anything is wrong it stops Liftbridge.
  • At first boot, if the CA, the certificates or the NATS login cannot be created, the first-boot marker is not written and Liftbridge does not start.
  • Liftbridge requires liftbridge-egress-guard.service: if the firewall table cannot be loaded, Liftbridge does not start.
sudo liftbridge-config-gate
systemctl show -p Requires --value liftbridge | tr ' ' '\n' | grep egress
liftbridge-config-gate: OK (liftbridge.service)
liftbridge-egress-guard.service

Liftbridge runs as the unprivileged liftbridge user in a systemd sandbox with no capabilities: a read-only system except /var/lib/liftbridge, a system-call filter, no writable and executable memory, and core dumps off.

Step 13: The instance metadata block and outbound traffic

The EC2 instance metadata service (169.254.169.254) hands out the instance's IAM role credentials to any process that asks, so a network service must not be able to reach it. liftbridge and liftbridge-firstboot run with the metadata addresses blocked by systemd (IPAddressDeny). Only liftbridge-metadata-names, which reads the instance's names for the server certificate, may reach the metadata service, and it may reach nothing else:

for u in liftbridge liftbridge-firstboot liftbridge-metadata-names; do printf '%-26s deny=%s allow=%s\n' "$u" "$(systemctl show -p IPAddressDeny --value "$u")" "$(systemctl show -p IPAddressAllow --value "$u")"; done
liftbridge                 deny=::ffff:169.254.169.254/128 ::ffff:168.63.129.16/128 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128 allow=
liftbridge-firstboot       deny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 allow=
liftbridge-metadata-names  deny=0.0.0.0/0 ::/0 allow=fd00:ec2::254/128 169.254.169.254/32

On top of that, the nftables table inet cloudimg_liftbridge_egress denies all outbound traffic, IPv4 and IPv6, for the liftbridge user. It may answer its clients and connect to its own NATS server on 127.0.0.1:4222, and nothing else: not the metadata service, the internet, your VPC, DNS or even its own port 9292. Other users are unaffected:

IP=$(sudo sed -n 's/^LIFTBRIDGE_PRIVATE_IP=//p' /root/liftbridge-credentials.txt)
for t in http://169.254.169.254/latest/meta-data/ https://1.1.1.1/ "http://$IP:22/"; do sudo -u liftbridge curl -s -m 5 -o /dev/null -w "liftbridge -> $t: %{http_code} %{errormsg}\n" "$t" || true; done
curl -s -m 5 -o /dev/null -w "ubuntu -> https://1.1.1.1/: %{http_code}\n" https://1.1.1.1/
liftbridge -> http://169.254.169.254/latest/meta-data/: 000 Failed to connect to 169.254.169.254 port 80 after 0 ms: Couldn't connect to server
liftbridge -> https://1.1.1.1/: 000 Failed to connect to 1.1.1.1 port 443 after 0 ms: Couldn't connect to server
liftbridge -> http://172.31.95.40:22/: 000 Failed to connect to 172.31.95.40 port 22 after 0 ms: Couldn't connect to server
ubuntu -> https://1.1.1.1/: 301

liftbridge-egress-guard.service loads the table before Liftbridge starts. Liftbridge needs no outbound connection: a single node has no peers, and telemetry is off.

Step 14: Use your own DNS name or a certificate from your CA

Point a DNS name at the instance (an Elastic IP keeps the address across stop and start), then make Liftbridge advertise it:

sudo liftbridge-set-hostname <dns-name>

This re-issues the server certificate (same key, from this instance's CA) to cover the name, updates LIFTBRIDGE_HOST and LIFTBRIDGE_ADDR in /root/liftbridge-credentials.txt and restarts Liftbridge. Clients keep their ca.crt and certificates.

To serve a server certificate from your own CA or a public one instead, copy the chain (server certificate first, then any intermediates), its unencrypted PEM key and the root CA certificate your clients will trust to the instance, then run:

sudo liftbridge-install-cert <certificate-chain.pem> <private-key.pem> <server-ca.pem> <dns-name>

Port 9292 then serves your certificate, <server-ca.pem> is copied to /root/liftbridge-client/server-ca.crt and named in LIFTBRIDGE_TLS_CA_FILE, and clients verify the server with it instead of ca.crt. Client certificates are unchanged: Liftbridge still requires one issued by this instance's CA. The boot-time renewal never replaces an installed certificate. sudo liftbridge-set-hostname <name> goes back to a new key and certificate from this instance's CA.

Step 15: How this build was made, and how to verify it

The upstream Liftbridge 26.01.1 release binary was built with Go 1.25.6, which is out of support, and with module versions that have known vulnerabilities reachable from Liftbridge's code. cloudimg therefore builds Liftbridge from the upstream source instead:

  • Source: the v26.01.1 tag, commit 50ac358b41eba51e695105436589958f8c4794d8, from https://github.com/liftbridge-io/liftbridge/archive/refs/tags/v26.01.1.tar.gz (SHA-256 c6cf76fb396c445333f41c3780f1f288040ed9431065c9215d815ffa651d47e0). No source file is changed.
  • Go: go1.27.3 (go1.27.3.linux-amd64.tar.gz, SHA-256 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40), because Go 1.25 no longer gets security fixes.
  • Module raises: github.com/nats-io/nats-server/v2 v2.12.15, google.golang.org/grpc v1.85.0, golang.org/x/net v0.62.0 and golang.org/x/crypto v0.58.0, then go mod tidy, which changes only go.mod and go.sum (SHA-256 1c53c5296f4fe63f108a674485c23ea2ec36e678c99f76d12c2a7be813809d64 and bc958bfa29d6f3c9b40aaa20283161d120c286a5fe75a1d5a299be3b9b283cce). Both files, the originals and the diffs are in /usr/share/doc/cloudimg-liftbridge/build/.
  • Build command (in the unpacked source with the raised go.mod and go.sum):
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOAMD64=v1 GOTOOLCHAIN=local GOFLAGS=-modcacherw \
  go build -mod=readonly -trimpath -buildvcs=false \
  -ldflags='-buildid= -X github.com/liftbridge-io/liftbridge/server.Version=26.01.1' -o liftbridge .
  • Result: /usr/local/bin/liftbridge, SHA-256 c1003adf40c2976454b7290629bd4ffca19cd9142d6f7658b810efa2e189eddb. The build is reproducible: the same source, Go and module files give the same bytes. lift-cli is a small cloudimg client on go-liftbridge v2.4.0 with the same raises, SHA-256 418e6f9891d1dbf0d0c3ea045f254bc7c9b306aeded5a8350f1829754b964256.
  • Checks at build time: govulncheck finds no reachable vulnerability in either program, in source and in binary mode, and Liftbridge's own short tests pass on the raised modules. No Go toolchain is left on the image.

Check it on your instance:

liftbridge --version
sha256sum /usr/local/bin/liftbridge /usr/local/bin/lift-cli
grep -E '^(build|source|toolchain|raises) ' /usr/share/doc/cloudimg-liftbridge/PROVENANCE | cut -c1-160
liftbridge version 26.01.1
c1003adf40c2976454b7290629bd4ffca19cd9142d6f7658b810efa2e189eddb  /usr/local/bin/liftbridge
418e6f9891d1dbf0d0c3ea045f254bc7c9b306aeded5a8350f1829754b964256  /usr/local/bin/lift-cli
build cloudimg build from source (coordinator ruling d-4065), not the upstream release binary
source https://github.com/liftbridge-io/liftbridge/archive/refs/tags/v26.01.1.tar.gz sha256 c6cf76fb396c445333f41c3780f1f288040ed9431065c9215d815ffa651d47e0 (tag v26.01.1
toolchain go1.27.3 https://go.dev/dl/go1.27.3.linux-amd64.tar.gz sha256 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40 (CGO_ENABLED=0 -trimpath -buildvc
raises github.com/nats-io/nats-server/v2@v2.12.15 google.golang.org/grpc@v1.85.0 golang.org/x/net@v0.62.0 golang.org/x/crypto@v0.58.0 + go mod tidy (no source patch); go.m

The licence texts of every module linked into the two programs are in /usr/share/doc/cloudimg-liftbridge/THIRD_PARTY_NOTICES.

Logs, files and limits

  • Server log: journalctl -u liftbridge (start-up and shutdown, the gate's and the guard's results; never a key or password). First boot: journalctl -u liftbridge-firstboot.
  • Configuration: /etc/liftbridge/liftbridge.yaml and /etc/liftbridge/nats.conf (root:liftbridge 0640, rendered at first boot and checked by the gate on every start). Server key and certificates: /etc/liftbridge/tls/. CA: /etc/cloudimg/liftbridge/ (root only).
  • A message is at most about 1 MiB (the embedded NATS server's payload limit), and NATS accepts at most 1,024 connections.
  • This image runs a single Liftbridge node, so each stream has one replica: protect the data with EBS snapshots. Clustering is not configured.

Support

cloudimg provides support for the image: launch problems, the first-boot setup, the certificates, the build and the cloudimg helpers. Contact support@cloudimg.co.uk. Liftbridge itself is documented at liftbridge.io and github.com/liftbridge-io/liftbridge.