13 KiB
Installation Guide
Requirements
- Operating systems: Linux (kernel 6.10+), FreeBSD (14.0+)
- Architecture: amd64; arm64 builds but is untested
- To build: Go 1.27.1 or newer (FreeBSD: the
go127package), and GNU make or BSD make - For
make e2e: bash 5+, coreutils, curl, openssl and go (the proxy test builds its reverse proxy). Optional, else those checks SKIP: node with playwright and the Chromium buildsplaywright install chromiumfetches (browser checks), an IPv6 loopback (ipv6-test.sh), docker as root with thelogwisp:devimage (deploy-test.sh's container run)
Building from Source
git clone https://github.com/lixenwraith/logwisp.git
cd logwisp
make build # bin/lw; plain `make` only lists the targets
The Makefile works with GNU make and BSD make alike. Targets:
- Build
make build:bin/lw, with version, commit and build time from gitmake release: the same, static (CGO_ENABLED=0),-trimpath, strippedmake dev: built with the race detectormake version: the metadata a build would embedmake clean: removebin/
- Check
make test: the Go testsmake verify: tests,go vet,gofmt -lon the Go files changed sincemain, linux and freebsd cross builds for amd64 and arm64, and the web client'snode --testwhen node is installedmake e2e: builds, then runs everytest/*-test.sh --autoin turn and reports passes, failures and skips;E2E='test/scram-*-test.sh'runs a subset; see Test Scripts
- Container
make image,make image-check: see Container Image
- Install
make install,make uninstall: see Installing
Variables:
GO: the toolchain. On FreeBSD the default is the versioned package thatgo.modasks for (go127);make GO=/path/to/gooverrides it, andcheck-gonames the package to install when it is missing.GO_BUILDFLAGS,GO_LDFLAGS: extrago buildand linker flags for packagers. They are distinct fromGOFLAGSandLDFLAGS, which the go command and the C linker read themselves.SOURCE_DATE_EPOCH: when set, the embedded build time comes from it, so package builds are reproducible.
A plain go build -o bin/lw ./cmd/lw works too, as does
go install github.com/lixenwraith/logwisp/cmd/lw@latest (or @vX.Y.Z).
Without the Makefile's -ldflags, lw --version reports what Go stamps into
the binary: the module version, or a pseudo-version with the commit in a
checkout.
Installing
make install copies what a package ships and compiles nothing, so build
first. It honours DESTDIR, PREFIX, SYSCONFDIR and BINDIR (default
$PREFIX/bin, the path the systemd unit and the rc.d script run lw from):
- Linux defaults:
PREFIX=/usr,SYSCONFDIR=/etc. Outside a package manager, preferPREFIX=/usr/local; systemd reads units, sysusers and tmpfiles from there too. - FreeBSD defaults:
PREFIX=/usr/local,SYSCONFDIR=/usr/local/etc. INSTALL_OS(default:uname -s) picks the layout, soINSTALL_OS=FreeBSD DESTDIR=...stages the FreeBSD one on another system.
make release
sudo make install PREFIX=/usr/local
Every system gets:
$BINDIR/lwand the manual$PREFIX/share/man/man1/lw.1$PREFIX/share/doc/logwisp/(this documentation) and$PREFIX/share/licenses/logwisp/LICENSE
Linux (systemd)
$SYSCONFDIR/logwisp/logwisp.toml: the annotated sample configuration, kept when one is already there. Edit it before starting the service.$PREFIX/lib/systemd/system/logwisp.service: runslw -c $SYSCONFDIR/logwisp/logwisp.tomlas thelogwispuser, from/var/lib/logwisp;systemctl reloadsendsSIGHUP.$PREFIX/lib/sysusers.d/logwisp.conf: thelogwispaccount.$PREFIX/lib/tmpfiles.d/logwisp.conf:/var/lib/logwispand/var/log/logwisp, owned by it.
sudo systemd-sysusers logwisp.conf
sudo systemd-tmpfiles --create logwisp.conf
sudo systemctl daemon-reload
sudo systemctl enable --now logwisp
The unit is hardened: no capabilities, a read-only view of the system with
only its two directories writable, /home hidden, and IPv4, IPv6 and Unix
sockets only. Widen it with a drop-in (systemctl edit logwisp) rather than
editing the unit:
- A
filesink orlogging.fileelsewhere:ReadWritePaths=/srv/logs - A
filesource under/home:ProtectHome=read-only - A port below 1024:
CapabilityBoundingSet=CAP_NET_BIND_SERVICEandAmbientCapabilities=CAP_NET_BIND_SERVICE
The service must read every key, credentials and password file it names.
Create a credentials file with its owner and mode first; lw auth keeps
both:
sudo install -m 0600 -o logwisp -g logwisp /dev/null /etc/logwisp/users.toml
sudo lw auth add-user --credentials /etc/logwisp/users.toml --user <name> \
--password-file <file>
sudo systemctl reload logwisp
FreeBSD (rc.d)
$SYSCONFDIR/logwisp/logwisp.toml.sample, copied tologwisp.tomlwhen none exists andDESTDIRis empty$PREFIX/etc/rc.d/logwisp: runslwthroughdaemon(8)aslogwisp_user, with its output in syslog under the taglogwisp;service logwisp reloadsendsSIGHUP
sudo pw useradd logwisp -d /nonexistent -s /usr/sbin/nologin -c "LogWisp log transport"
sudo sysrc logwisp_enable=YES
sudo service logwisp start
rc.conf variables:
logwisp_enable:YESto start at boot (defaultNO)logwisp_config: the configuration (default/usr/local/etc/logwisp/logwisp.toml)logwisp_user: the account (defaultlogwisp)logwisp_chdir: the working directory (default/var/db/logwisp, created forlogwisp_userwhen missing)logwisp_args: extralwoptions, e.g.--logging.level=debug(default empty);logwisp_flagsgoes todaemon(8), not tolw
Uninstall
sudo make uninstall (with the PREFIX used to install) removes everything
make install wrote except $SYSCONFDIR/logwisp, which holds configuration
and credentials. Remove that, the working directories and the account by
hand.
Container Image
The Dockerfile builds lw in a Go builder and copies it alone, static and
stripped, into scratch: /lw running as UID 65532, plus the CA bundle that
TLS dialers without a ca_file verify against. There is no shell and no
configuration in the image.
make image # logwisp:dev; IMAGE=, IMAGE_TAG= rename it
make image-check
image-check runs the image the way it should run in production: read-only,
--network none, --cap-drop ALL, no-new-privileges, as 65532. It prints
--version, then runs --check on config/logwisp.toml (or
IMAGE_CHECK_CONFIG) mounted read-only.
Building behind a proxy:
- A TLS-intercepting proxy:
make image BUILD_CA=/path/to/ca-bundle.pem. The bundle reaches only the module download, as a BuildKit secret. - An explicit proxy: pass Docker's predefined proxy build arguments, which
never reach an image layer, e.g.
IMAGE_BUILD_FLAGS='--network host --build-arg HTTPS_PROXY=http://127.0.0.1:<port>'. CONTAINER_ENGINE=podmanworks too.
Running it securely
docker run -d --name logwisp \
--read-only --cap-drop ALL --security-opt no-new-privileges \
-v /etc/logwisp:/etc/logwisp:ro \
-v /srv/logwisp/secrets:/run/secrets:ro \
-p 8443:8443 \
-e LOGWISP_LOGGING_LEVEL=info \
logwisp:<tag> -c /etc/logwisp/logwisp.toml
-
Configuration: mount it read-only at
/etc/logwispand name it with-c. The files must be readable by UID 65532. -
Scalar settings: environment variables,
LOGWISP_<PATH>. -
Pipelines without a file: the pipeline variables, in the
--sourceand--sinksyntax (see CLI); they replace the file's pipelines:-e LOGWISP_PIPELINE=relay \ -e LOGWISP_SOURCE=tcp_chain,port=9000,tls.enabled=true,tls.cert_file=/run/secrets/tls.crt,tls.key_file=/run/secrets/tls.key,auth.type=scram,auth.credentials_file=/run/secrets/users.toml \ -e LOGWISP_SINK=consoleWithout a file lw logs warnings only and runs no status reporter; add
-e LOGWISP_LOGGING_LEVEL=info(and-e LOGWISP_STATUS_REPORTER=true) for the service defaults. The image cannot set them: a variable would override a mounted file's value. Without-i, stdin is/dev/null, so a container needs a source other thanconsole. -
Secrets: only as mounted files, at
/run/secrets(Docker or Compose secrets, a Kubernetes secret volume), named bytls.key_file,auth.credentials_fileandauth.password_file. Never put a password in the environment:docker inspectand/procshow it. Make each file readable by 65532 only (chown 65532and mode0400). -
Privileges: keep the image's non-root user, a read-only root filesystem,
--cap-drop ALLandno-new-privileges. Without capabilities a listener needs a port of 1024 or above; publish it on any host port. -
Listeners bind
0.0.0.0inside the container;127.0.0.1would be unreachable through-p. -
Writable data: a
filesink orlogging.fileneeds a volume writable by 65532; nothing else is written. -
Reload:
docker kill -s HUP logwisp. -
lw authruns from the image too, with directories writable by 65532. Keep the generated passwords out of the directory the server mounts: it needs onlyusers.toml(verifiers) and the TLS files, while a.passfile is the client's plaintext password.docker run --rm --read-only --cap-drop ALL --network none \ -v /srv/logwisp/secrets:/work -v /srv/logwisp/passwords:/pw logwisp:<tag> \ auth add-user --credentials /work/users.toml --user <name> --password-file /pw/<name>.pass
Packaging Status
The foundation for distribution packages is in place:
- the
lwbinary name, andmake installwithDESTDIR,PREFIXandSYSCONFDIR - the
doc/lw.1manual - service files in
deploy/package/:logwisp.service,logwisp.sysusers,logwisp.tmpfilesand the FreeBSDlogwisp.rc - skeletons:
deploy/package/arch/PKGBUILDand asysutils/logwispport indeploy/package/freebsd/
Still missing: release tags, which both skeletons download; finishing and submitting the AUR package and the port; Debian packaging. To Do has the steps.
Verification
lw --version
lw -c /etc/logwisp/logwisp.toml --logging.level=debug --logging.output=stderr
sudo systemctl status logwisp # Linux
sudo service logwisp status # FreeBSD
Expect Created source instance, Created sink instance and
Starting pipeline for each pipeline. To validate a configuration without
starting it, run lw --check -c FILE; see
Operations.
Test Scripts
The end-to-end scripts in test/ run against bin/lw and share
test/lib.sh. make e2e runs each with --auto and reports it as PASS, FAIL
or SKIP (exit 77, e.g. no IPv6 loopback); only a failure fails the target.
- Modes:
- without flags: start the daemons, print a guide of the ports and one-line
commands (run
. test/run-X/envfirst for$LW,$CAand the token helper), and wait for Ctrl-C --auto: run the checks and tear down;--keepleaves the daemons up after a pass--follow: show only WARN, ERROR and key events from the daemons
- without flags: start the daemons, print a guide of the ports and one-line
commands (run
- Daemons log only to
test/run*/log/NAME.out, never to the terminal. - Colour only on a terminal;
NO_COLORturns it off,FORCE_COLOR=1on. - Exit status: 0 all passed, 1 a failure, 77 skipped.
- Each script owns a port range and a gitignored run directory:
15801-15804 chain-test.sh, chain-aggregate-test.sh test/run/
(no ports) passthrough-test.sh test/run/passthrough/
15811-15814 mtls-chain-test.sh test/run-mtls/
15821-15825 scram-chain-test.sh test/run-scram/
15831-15832 scram-proxy-test.sh test/run-proxy/
15851-15855 ipv6-test.sh test/run-ipv6/
15871-15879 deploy-test.sh test/run-deploy/
15881-15889 acl-test.sh test/run/acl/
chain-test.sh: two edges into a relay, one pipeline per chain transport; ports 15801-15804chain-aggregate-test.sh: the same edges, with the relay fanning both into both sinks; ports 15801-15804passthrough-test.sh:filesource tofilesink, a wide envelope byte for byte (raw = true) and, parsed, with no key dropped; no portsmtls-chain-test.sh: mTLS chain edges and viewers, allow lists, node binding; ports 15811-15814scram-chain-test.sh: SCRAM chain edges and viewers withlw authcredentials; ports 15821-15825scram-proxy-test.sh: browser and CLI logins to anhttpsink behind a TLS-terminating proxy; ports 15831-15832; the browser checks skip without node and playwright's Chromium: the headless shell for the scenarios with cookies, full Chromium for the profile that blocks themipv6-test.sh: a relay on::1over both chain transports, IPv6 SANs, per-family listeners; ports 15851-15855; skips (exit 77) without an IPv6 loopbackdeploy-test.sh: the configurationsdeploy/lw-deploy.shwrites, deployed and checked; ports 15871-15879acl-test.sh: each listener admitting or refusing peers by address before TLS, with clients from 127.0.0.1 and 127.0.0.2; ports 15881-15889