Why do you need containers? "just run it as a systemd service with super-long-list of options to lock it down", they said.
Anyway, I run Arch Linux, the unofficial testing ground of systemd, so we have all the latest bugs as well. As always, RTFM: system.unit systemd.service systemd.exec systemd.resource-control
So, what are we containing? A very generic web app exercising the network, filesystem, and secrets.
important: build as a static binary (CGO_ENABLED=0
)
main.go
:
1package main
2
3import (
4 "flag"
5 "io"
6 "log"
7 "net/http"
8 "os"
9 "path/filepath"
10)
11
12func main() {
13 var configPath, dataPath, certFile, keyFile string
14 flag.StringVar(&configPath, "config", "", "path to config file")
15 flag.StringVar(&dataPath, "data", "", "path to data dir")
16 flag.StringVar(&certFile, "cert", "", "path to cert file")
17 flag.StringVar(&keyFile, "key", "", "path to key file")
18 flag.Parse()
19
20 log.Printf("starting with config=%q data=%q", configPath, dataPath)
21 b, err := os.ReadFile(configPath)
22 if err != nil {
23 log.Fatal("read config", err)
24 }
25 log.Println("config:\n", string(b))
26
27 statefile := filepath.Join(dataPath, "statefile")
28
29 http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
30 if r.Method == http.MethodGet {
31 b, err := os.ReadFile(statefile)
32 if err != nil {
33 rw.WriteHeader(http.StatusInternalServerError)
34 rw.Write([]byte(err.Error()))
35 return
36 }
37 rw.Write(b)
38 } else if r.Method == http.MethodPost {
39 defer r.Body.Close()
40 b, err := io.ReadAll(r.Body)
41 if err != nil {
42 rw.WriteHeader(http.StatusInternalServerError)
43 rw.Write([]byte(err.Error()))
44 }
45 err = os.WriteFile(statefile, b, 0o644)
46 if err != nil {
47 rw.WriteHeader(http.StatusInternalServerError)
48 rw.Write([]byte(err.Error()))
49 }
50 rw.Write([]byte("ok"))
51 }
52 })
53
54 log.Println(http.ListenAndServeTLS(":8080", certFile, keyFile, nil))
55}
So what do you ship to your target system? A binary and a systemd service file of course. No need for extra user, directory, etc.. setup, but you'll want to provide your config files, and secrets.
foobar.service
:
1[Unit]
2Description=A foo bar app
3Documentation=https://seankhliao.com/blog/12021-04-07-hardened-systemd-service/
4Requires=network-online.target
5After=network-online.target
6
7[Service]
8Type=simple
9# path within the filesystem namespace
10ExecStart=/bin/foobar -config /etc/foobar/conf.yaml -data /var/lib/foobar -cert ${CREDENTIALS_DIRECTORY}/cert -key ${CREDENTIALS_DIRECTORY}/key
11RestartSec=60s
12Restart=always
13
14WorkingDirectory=/
15# path on host system to be root of filesystem namespace,
16# created by RuntimeDirectory=
17# There's nothing in this namespace, including no libc,
18# which is why statically linked binaries are important
19# otherwise the error is a very opaque 203/EXEC File not found
20RootDirectory=/run/foobar
21ProtectProc=noaccess
22ProcSubset=pid
23# mount our executable from host into namespace
24BindReadOnlyPaths=/usr/local/bin/foobar:/bin/foobar
25
26DynamicUser=true
27
28CapabilityBoundingSet=
29AmbientCapabilities=
30
31NoNewPrivileges=true
32
33UMask=0022
34
35ProtectSystem=strict
36ProtectHome=true
37RuntimeDirectory=foobar
38StateDirectory=foobar
39# CacheDirectory=
40# LogDirectory=
41ConfigurationDirectory=foobar
42PrivateTmp=true
43PrivateDevices=true
44PrivateIPC=true
45PrivateUsers=true
46ProtectHostname=true
47ProtectClock=true
48ProtectKernelTunables=true
49ProtectKernelModules=true
50ProtectKernelLogs=true
51ProtectControlGroups=true
52RestrictAddressFamilies=AF_INET AF_INET6
53RestrictNamespaces=true
54LockPersonality=true
55MemoryDenyWriteExecute=true
56RestrictRealtime=true
57RestrictSUIDSGID=true
58RemoveIPC=true
59PrivateMounts=true
60
61# I think this is everything most Go processes would need
62# maybe you can trim it down more?
63# SystemCallFilter=@basic-io @file-system @io-event @network-io @sync
64SystemCallErrorNumber=EPERM
65SystemCallArchitectures=native
66
67# unset all env
68Environment=
69
70# Fancy credential passing
71LoadCredential=cert:/etc/certs/example.com.pem
72LoadCredential=key:/etc/certs/example.com-key.pem
73
74IPAddressAllow=any
75IPAddressDeny=
76DevicePolicy=closed
77DeviceAllow=
78
79
80[Install]
81WantedBy=mult-user.target
So if you make outgoing network calls, you'll want DNS and ca-certs. Also might want tzdata? but Go can embed that these days.
1BindReadOnlyPaths=/usr/bin/feed-agg:/bin/feed-agg \
2 /etc/feed-agg:/etc/feed-agg:rbind \
3 /etc/resolv.conf:/etc/resolv.conf \
4 /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt