19 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.2 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 wasm:bin/lwconf.wasm, the configuration engine for the website, version stamped, beside the toolchain'swasm_exec.js; prints both checksumsmake version: the metadata a build would embedmake completion: regenerate the bash, zsh and fish completion indeploy/package/completion/fromcmd/lw'sshortsandcommandstables;go test ./cmd/lwfails while they differmake 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 js/wasm one, and, when node is installed,node --testfor the web client and forcmd/lwconf(which runsmake wasm)make 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,make deb,make arch: 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, BINDIR (default
$PREFIX/bin, the path the systemd unit and the rc.d script run lw from)
and SERVICE:
- 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.SERVICE:yes(default) adds the service and its configuration;noinstallslwalone (Without a Service).BASHCOMPDIR,ZSHCOMPDIR,FISHCOMPDIR: where the shell completion goes, by default$PREFIX/share/bash-completion/completions,$PREFIX/share/zsh/site-functions(/usr/share/zsh/vendor-completionsforPREFIX=/usron Debian and Ubuntu, whose zsh reads only that) and$PREFIX/share/fish/vendor_completions.d.
make release
sudo make install PREFIX=/usr/local
Every install gets:
$BINDIR/lwand the manual$PREFIX/share/man/man1/lw.1- completion for
lw, its commands, flags, files and preset names:lw,_lwandlw.fishin the three directories above $PREFIX/share/doc/logwisp/(this documentation) and$PREFIX/share/licenses/logwisp/LICENSE
A service install also gets the service files below and, on Linux and
FreeBSD alike, $SYSCONFDIR/logwisp/logwisp.toml: the annotated sample
configuration, placed when none is there and otherwise kept. Edit it before
starting the service.
Linux (systemd)
$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)
$PREFIX/etc/rc.d/logwisp runs lw through daemon(8) as logwisp_user,
with its output in syslog under the tag logwisp; service logwisp reload
sends SIGHUP. The port in deploy/package/freebsd/ places the
configuration the ports way, to the same effect: @sample installs
logwisp.toml.sample and copies it to logwisp.toml when none exists.
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
Debian and Ubuntu (.deb)
make deb builds a release and packages what make install stages, in
Debian's layout, as bin/logwisp_VERSION_ARCH.deb, the architecture taken
from the binary. A release tag gives the version; an untagged or modified
tree's sorts below every release and by build time, so each build upgrades
the last. DEB_MAINTAINER names the maintainer.
make deb
sudo apt install ./bin/logwisp_*.deb
sudo systemctl enable --now logwisp
- Installing creates the
logwispaccount and its directories wheresystemd-sysusersandsystemd-tmpfilesexist; the service waits to be enabled, and an upgrade or reinstall restarts it if it runs. /etc/logwisp/logwisp.tomlis a conffile: an upgrade or reinstall keeps your edits, and only a purge removes it.- The same version again:
sudo apt install --reinstall ./bin/logwisp_*.deb. - Removing stops and disables the service. The account, its directories
and any credentials in
/etc/logwispstay, as withmake uninstall.
Arch Linux (makepkg)
make arch builds the PKGBUILD in deploy/package/arch/ from the working
tree, committed or not, into bin/arch/, then prints the pacman -U line
that installs the package. It needs base-devel and go, and runs as a
user: makepkg refuses root.
make arch
sudo pacman -U bin/arch/logwisp-<version>-1-x86_64.pkg.tar.zst
sudo systemctl enable --now logwisp
- pacman's hooks create the
logwispaccount and its directories; the service waits to be enabled. /etc/logwisp/logwisp.tomlis abackupfile: an upgrade or reinstall keeps your edits, and a shipped version that changed lands beside it aslogwisp.toml.pacnew.pacman -Rkeeps an edited one aslogwisp.toml.pacsave.- Reinstall:
make archand the samepacman -U. pacman does not restart the service:sudo systemctl restart logwisp. - An earlier
sudo make installconflicts with the package (PREFIX=/usr) or overrides it (PREFIX=/usr/local, whose unit andlwcome first):sudo make uninstallwith thatPREFIXfirst. The configuration it keeps is no conflict: pacman keeps it, and when it was edited, the shipped one lands beside it aslogwisp.toml.pacnew.
Reinstalling
Every method runs again over its own installation, for an upgrade or the
same version, and keeps an edited configuration: make install leaves an
existing $SYSCONFDIR/logwisp/logwisp.toml alone, and the packages treat it
as above. Only the .deb restarts a running service; otherwise restart it
(sudo systemctl restart logwisp, sudo service logwisp restart). To see
what a release changed in the shipped configuration:
d=$(mktemp -d); lw config init --out "$d/logwisp.toml"; diff "$d/logwisp.toml" /etc/logwisp/logwisp.toml
Without a Service
SERVICE=no installs lw, its manual, completion and documentation, and
nothing else: no service files, account or configuration. For one user,
without root:
make release
make install SERVICE=no PREFIX="$HOME/.local"
lw config init
~/.local/bingoes onPATH.man lwthen finds the manual, and bash and fish the completion; zsh needsfpath=(~/.local/share/zsh/site-functions $fpath)beforecompinit.make uninstall PREFIX="$HOME/.local"removes it.
For every user: sudo make install SERVICE=no PREFIX=/usr/local.
lw config init writes the shipped, annotated configuration, never over an
existing file, where you choose:
lw config init:~/.config/logwisp/logwisp.toml, whichlwreads without-cunless aLOGWISP_CONFIG_*variable names another file. With it in place,lwwithout pipeline options runs its pipelines instead of copying stdin to stdout; pipeline options and presets still replace them.sudo lw config init --out /usr/local/etc/logwisp/logwisp.toml: a file for every user, named with-c, or without it byLOGWISP_CONFIG_DIR=/usr/local/etc/logwisp(File Location).
lw --check (with the same -c) then validates it.
Uninstall
sudo make uninstall (with the PREFIX used to install) removes everything
make install wrote except $SYSCONFDIR/logwisp, which holds configuration
and credentials, and any file lw config init wrote. Remove those, 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
In place:
make installwithDESTDIR,PREFIX,SYSCONFDIRandSERVICE, the manual, shell completion and the service files indeploy/package/make deb, andmake archbuilding the PKGBUILD from the working tree- skeletons:
deploy/package/arch/PKGBUILDand asysutils/logwispport indeploy/package/freebsd/ .github/workflows/package.yml, on changes to them:- Arch container:
make arch, installed over an edited configuration, reinstalled keeping it, removed - Ubuntu: the
.debinstalled, started, reinstalled keeping an edited configuration, removed and purged - Ubuntu, without a service:
make install SERVICE=nointo a home twice, thenlw config initwriting the filelwreads, once - FreeBSD: BSD make building, installing twice keeping an edited configuration, and running the rc.d service
make image image-check
- Arch container:
Still missing: release tags, which both skeletons download; submitting the AUR package and the port; a Debian source package for a PPA or the archive. 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, then by the client a PROXY header names behind a stub proxy on 127.0.0.11-13 (and nginxstreamon 127.0.0.14 when installed) or an L7 proxy'sX-Forwarded-For, with per-client connection caps, request rates and login throttling; ports 15881-15889; skips (exit 77) where 127.0.0.2 cannot be bound