diff --git a/cmd/hepto.go b/cmd/hepto.go
index 52d3388e2097eaf4b2cfc5b3ab81d4b7892fcfc1..163537f01df136044c46503343459c90ce624b69 100644
--- a/cmd/hepto.go
+++ b/cmd/hepto.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+  "fmt"
 	"os"
 	"path/filepath"
 	"strings"
@@ -15,7 +16,6 @@ import (
 	_ "github.com/containerd/containerd/runtime/v2/runc/task/plugin"
 	shimv2 "github.com/containerd/containerd/runtime/v2/shim"
 	runc "github.com/opencontainers/runc/cmd"
-	"github.com/sirupsen/logrus"
 
 	//hostlocal "github.com/containernetworking/plugins/plugins/ipam/host-local"
 	//"github.com/containernetworking/plugins/plugins/main/bridge"
@@ -28,7 +28,7 @@ import (
 
 func main() {
 	bin := filepath.Base(os.Args[0])
-  var err error
+	var err error
 	if bin == "mount" {
 		// Hook the mount command for mounting configmaps
 		// This is fairly naive mount implementation, kubelet only evers calls
@@ -45,14 +45,14 @@ func main() {
 	} else if bin == "containerd-shim-runc-v2" || (bin == "exe" && len(os.Args) > 1 && os.Args[1] == "-namespace") {
 		// Run the containerd shim
 		// It is also available under hepto name, for similar reasons as
-    // containerd, hence the different guess condition
-    plugins := plugin.Graph(func(*plugin.Registration) bool {return false})
-    for _, plug := range plugins {
-      plug.Disable = !strings.HasPrefix(plug.URI(), "io.containerd.ttrpc")
-    }
+		// containerd, hence the different guess condition
+		plugins := plugin.Graph(func(*plugin.Registration) bool { return false })
+		for _, plug := range plugins {
+			plug.Disable = !strings.HasPrefix(plug.URI(), "io.containerd.ttrpc")
+		}
 		shimv2.RunManager(context.Background(), manager.NewShimManager("io.containerd.runc.v2"))
-  } else if bin == "runc" {
-    runc.Run()
+	} else if bin == "runc" {
+		runc.Run()
 	} else if bin == "ctr" {
 		// Run containerd cli client, for debugging purposes
 		err = ctr.New().Run(os.Args)
@@ -61,10 +61,10 @@ func main() {
 		err = kubectl.NewDefaultKubectlCommand().Execute()
 	} else {
 		// If no hook ran a different command, simply run hepto
-    logrus.Info("running hepto main code")
 		hepto.Execute()
 	}
-  if err != nil {
-    logrus.Fatal(err)
-  }
+	if err != nil {
+    fmt.Printf("unexpected error: %v", err)
+    os.Exit(1)
+	}
 }
diff --git a/cmd/hepto/config.go b/cmd/hepto/config.go
index 0724439855110f037dbc4350ddba78a679e010ec..6555500c24beaeed05bd6689a01e4c7bfef061c6 100644
--- a/cmd/hepto/config.go
+++ b/cmd/hepto/config.go
@@ -7,12 +7,16 @@ import (
 
 	"forge.tedomum.net/acides/hepto/hepto/pkg/cluster"
 	"forge.tedomum.net/acides/hepto/hepto/pkg/selfcontain"
+	"github.com/go-logr/logr"
+  "github.com/go-logr/zapr"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
+	"go.uber.org/zap"
 )
 
 type Config struct {
 	DataDir   string
+  Logger    logr.Logger
 	Cluster   cluster.ClusterSettings
 	Container selfcontain.Config
 	Node      cluster.NodeSettings
@@ -67,12 +71,22 @@ func (c Config) dataPath(subPath string) string {
 	return dir
 }
 
-func (c *Config) Complete() {
+func (c *Config) Complete() error {
+  // Initialize logging
+  logger, err := zap.NewProduction() 
+  if err != nil {
+    return err
+  }
+  c.Logger = zapr.NewLogger(logger)
+  c.Container.Logger = c.Logger
+  c.Cluster.Logger = c.Logger
+  // Setup paths and container settings
 	c.Container.Root = c.dataPath("root")
 	c.Container.Data = c.dataPath("containers")
 	c.Container.Name = c.Node.Name
   c.Container.Capabilities = additionalCapabilities
   c.Container.Devices = additionalDevices
+  return nil
 }
 
 var config Config
diff --git a/cmd/hepto/root.go b/cmd/hepto/root.go
index 05fa7de4275856143aa63a859ad5d3744e5a7b88..f992c51d67d34a9b2d3b2de38c0b034edd4d4e44 100644
--- a/cmd/hepto/root.go
+++ b/cmd/hepto/root.go
@@ -8,7 +8,6 @@ import (
 
 	"forge.tedomum.net/acides/hepto/hepto/pkg/cluster"
 	"forge.tedomum.net/acides/hepto/hepto/pkg/selfcontain"
-	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 )
 
@@ -18,29 +17,29 @@ var rootCmd = &cobra.Command{
 	Long: `Hepto is a Kubernetes distribution designed for geo-distributed
 	deployments, including across links with noticeable latency.`,
 	Run: func(cmd *cobra.Command, args []string) {
-    cluster.SetupLogging()
 		config.Complete()
 		err := selfcontain.RunFun(&config.Container, func() {
 			config.Node.IP = waitForIP()
-			logrus.Debug("current IP is ", config.Node.IP.String())
+			config.Logger.Info("found current IP", "ip", config.Node.IP.String())
 			c := cluster.New(&config.Cluster, &config.Node)
 			c.Run()
 		})
 		if err != nil {
-			logrus.Fatal(err)
+			config.Logger.Error(err, "could not initialize the wrapping container")
 		}
 	},
 }
 
 // Guess the current IP address
 func waitForIP() net.IP {
-	logrus.Info("determining current IP address...")
+	config.Logger.Info("determining current IP address")
 	for {
 		time.Sleep(time.Second)
-		logrus.Debug("sending a probe UDP packet to ", defaultDNS[0].String())
-		conn, err := net.Dial("udp", fmt.Sprintf("[%s]:53", defaultDNS[0].String()))
+    target := fmt.Sprintf("[%s]:53", config.Container.DNS[0].String())
+		config.Logger.Info("connecting outbound to guess IP", "target", target)
+		conn, err := net.Dial("udp", fmt.Sprintf("[%s]:53", target))
 		if err != nil {
-			logrus.Debug(err)
+			config.Logger.Error(err, "could not connect")
 			continue
 		}
 		localAddr := conn.LocalAddr().(*net.UDPAddr).IP
diff --git a/go.mod b/go.mod
index 02e6a99e01efc15701930dd68b12cfc92b0e88e2..a1fda9fa13dcc2cc070743c442e2dba40d156221 100644
--- a/go.mod
+++ b/go.mod
@@ -37,26 +37,24 @@ replace (
 )
 
 require (
-	github.com/bombsimon/logrusr/v3 v3.1.0
 	github.com/containerd/containerd v1.6.8
-	github.com/containerd/go-runc v1.0.0
-	github.com/containerd/ttrpc v1.1.1-0.20220420014843-944ef4a40df3
 	github.com/containernetworking/plugins v1.1.1
 	github.com/go-logr/logr v1.2.3
+	github.com/go-logr/zapr v1.2.3
 	github.com/hashicorp/memberlist v0.4.0
 	github.com/opencontainers/runc v1.1.4
 	github.com/pkg/errors v0.9.1
-	github.com/sirupsen/logrus v1.9.0
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.13.0
 	github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5
 	go.etcd.io/etcd/server/v3 v3.5.5
+	go.uber.org/zap v1.19.0
 	golang.org/x/net v0.0.0-20220920152717-4a395b0a80a1
 	golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220916014741-473347a5e6e3
+	k8s.io/client-go v0.26.0-alpha.1
 	k8s.io/component-base v0.26.0-alpha.1
-	k8s.io/klog v1.0.0
 	k8s.io/kubectl v0.0.0
 	k8s.io/kubernetes v1.25.1
 )
@@ -104,8 +102,10 @@ require (
 	github.com/containerd/continuity v0.3.0 // indirect
 	github.com/containerd/fifo v1.0.0 // indirect
 	github.com/containerd/go-cni v1.1.6 // indirect
+	github.com/containerd/go-runc v1.0.0 // indirect
 	github.com/containerd/imgcrypt v1.1.5-0.20220421044638-8ba028dca028 // indirect
 	github.com/containerd/nri v0.1.0 // indirect
+	github.com/containerd/ttrpc v1.1.1-0.20220420014843-944ef4a40df3 // indirect
 	github.com/containerd/typeurl v1.0.3-0.20220422153119-7f6e6d160d67 // indirect
 	github.com/containernetworking/cni v1.1.1 // indirect
 	github.com/containers/ocicrypt v1.1.3 // indirect
@@ -224,6 +224,7 @@ require (
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
 	github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646 // indirect
+	github.com/sirupsen/logrus v1.9.0 // indirect
 	github.com/soheilhy/cmux v0.1.5 // indirect
 	github.com/spf13/afero v1.8.2 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
@@ -267,7 +268,6 @@ require (
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
-	go.uber.org/zap v1.19.0 // indirect
 	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
 	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
 	golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
@@ -296,7 +296,6 @@ require (
 	k8s.io/apimachinery v0.26.0-alpha.1 // indirect
 	k8s.io/apiserver v0.26.0-alpha.1 // indirect
 	k8s.io/cli-runtime v0.26.0-alpha.1 // indirect
-	k8s.io/client-go v0.26.0-alpha.1 // indirect
 	k8s.io/cloud-provider v0.26.0-alpha.1 // indirect
 	k8s.io/cluster-bootstrap v0.0.0 // indirect
 	k8s.io/component-helpers v0.26.0-alpha.1 // indirect
diff --git a/go.sum b/go.sum
index c2e177e4d1b4282c50c3174e63630d63f4d6e72e..51ddf0b9a44232b160362a4afdfc11e42c72e1b4 100644
--- a/go.sum
+++ b/go.sum
@@ -152,8 +152,6 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
-github.com/bombsimon/logrusr/v3 v3.1.0 h1:zORbLM943D+hDMGgyjMhSAz/iDz86ZV72qaak/CA0zQ=
-github.com/bombsimon/logrusr/v3 v3.1.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
 github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
@@ -360,6 +358,7 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A=
 github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
 github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
 github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
@@ -1668,8 +1667,6 @@ k8s.io/csi-translation-lib v0.26.0-alpha.1/go.mod h1:c9kcHanouJ7E/kZdDoxOnYqMl22
 k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/gengo v0.0.0-20220902162205-c0856e24416d/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
-k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
-k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
 k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
diff --git a/pkg/cluster/certs.go b/pkg/cluster/certs.go
index fe03d3bd53c2334cf16195466ff23b5f63e8beaa..1fe03e96a406313061914593a84c9d9a1c1431ec 100644
--- a/pkg/cluster/certs.go
+++ b/pkg/cluster/certs.go
@@ -1,8 +1,9 @@
 package cluster
 
 import (
+  "os"
+
 	"forge.tedomum.net/acides/hepto/hepto/pkg/pki"
-	"github.com/sirupsen/logrus"
 )
 
 func (c *Cluster) initCerts() {
@@ -10,11 +11,13 @@ func (c *Cluster) initCerts() {
 	if c.node.Role == Master {
 		ca, err := pki.NewClusterCA("/pki")
 		if err != nil {
-			logrus.Fatal("could not initialize pki: ", err)
+      c.settings.Logger.Error(err, "could not initialize pki")
+      os.Exit(1)
 		}
 		masterCerts, err := pki.NewMasterCerts("/master", c.networking.NodeAddress.IP)
 		if err != nil {
-			logrus.Fatal("could not initialize master certs: ", err)
+			c.settings.Logger.Error(err, "could not initialize master certs")
+      os.Exit(1)
 		}
 		c.pki = ca
 		c.masterCerts = masterCerts
@@ -22,7 +25,8 @@ func (c *Cluster) initCerts() {
 	} else {
 		ca, err := pki.EmptyClusterCA("/pki")
 		if err != nil {
-			logrus.Fatal("could not initialize pki: ", err)
+			c.settings.Logger.Error(err, "could not initialize pki")
+      os.Exit(1)
 		}
 		c.pki = ca
 	}
@@ -30,7 +34,8 @@ func (c *Cluster) initCerts() {
 	// Initialize node certificates
 	certs, err := pki.NewNodeCerts("/certs", c.node.Name)
 	if err != nil {
-		logrus.Fatal("could not initialize node certificates: ", err)
+    c.settings.Logger.Error(err, "could not initialize node certs")
+    os.Exit(1)
 	}
 	c.certs = certs
 	c.ml.State.Certificates = make(map[string]*pki.NodeCerts)
diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go
index 64d0451dd2b2d550b291b22fd10d1183e4a1ecbc..552c775e15362163b4a1e3345cb839e89f8d8c1c 100644
--- a/pkg/cluster/cluster.go
+++ b/pkg/cluster/cluster.go
@@ -3,31 +3,37 @@
 package cluster
 
 import (
+	"context"
+	"os"
+
 	"forge.tedomum.net/acides/hepto/hepto/pkg/pki"
 	"forge.tedomum.net/acides/hepto/hepto/pkg/sml"
 	"forge.tedomum.net/acides/hepto/hepto/pkg/wg"
-	"github.com/sirupsen/logrus"
 )
 
 type Cluster struct {
-	settings    *ClusterSettings
-	ml          *sml.Memberlist[HeptoMeta, HeptoState, *HeptoMeta, *HeptoState]
-	vpn         *wg.Wireguard
-	networking  *ClusterNetworking
-	node        *NodeSettings
-	certs       *pki.NodeCerts
-	masterCerts *pki.MasterCerts
-	pki         *pki.ClusterCA
-	services    *ClusterServices
+	settings        *ClusterSettings
+	ml              *sml.Memberlist[HeptoMeta, HeptoState, *HeptoMeta, *HeptoState]
+	vpn             *wg.Wireguard
+	networking      *ClusterNetworking
+	node            *NodeSettings
+	certs           *pki.NodeCerts
+	masterCerts     *pki.MasterCerts
+	pki             *pki.ClusterCA
+	servicesStarted bool
+	servicesDone    chan struct{}
+  serviceCtx      context.Context
 }
 
 func New(settings *ClusterSettings, node *NodeSettings) *Cluster {
 	return &Cluster{
-		settings:   settings,
-		node:       node,
-		networking: NewClusterNetworking(settings.Name, node.Name),
-		ml:         sml.New[HeptoMeta, HeptoState](node.Name, node.IP, node.Port, node.Anchors, settings.Key),
-		services:   NewClusterServices(),
+		settings:        settings,
+		node:            node,
+		networking:      NewClusterNetworking(settings.Name, node.Name),
+		ml:              sml.New[HeptoMeta, HeptoState](node.Name, node.IP, node.Port, node.Anchors, settings.Key, settings.Logger),
+		servicesDone:    make(chan struct{}),
+		servicesStarted: false,
+    serviceCtx: context.Background(),
 	}
 }
 
@@ -42,7 +48,8 @@ func (c *Cluster) Run() {
 	events := c.ml.Events()
 	err := c.ml.Start()
 	if err != nil {
-		logrus.Fatal("could not start memberlist: ", err)
+		c.settings.Logger.Error(err, "could not start memberlist")
+		os.Exit(1)
 	}
 	instr := c.ml.Instr()
 	instrUpdates := instr.Updates()
@@ -52,12 +59,13 @@ func (c *Cluster) Run() {
 		case <-events:
 			c.handlePKI()
 			c.updateVPN()
-			c.services.Update(c)
+			c.updateServices()
 		case <-instrUpdates:
 			c.networking.MTU = instr.MinMTU()
 			c.updateVPN()
-		case <-c.services.Done():
-			logrus.Fatal("a service stopped unexpectedly")
+		case <-c.servicesDone:
+			c.settings.Logger.Info("a service stopped unexpectedly")
+			os.Exit(1)
 		}
 	}
 }
diff --git a/pkg/cluster/config.go b/pkg/cluster/config.go
index a5a90914a542edce12a0ba394309bc10e66955a8..a9f8a6507279bba5c04dcf73ca9915c7b3cf6b0f 100644
--- a/pkg/cluster/config.go
+++ b/pkg/cluster/config.go
@@ -3,9 +3,13 @@ package cluster
 import (
 	"errors"
 	"net"
+
+	"github.com/go-logr/logr"
 )
 
 type ClusterSettings struct {
+  // Logger interface
+  Logger logr.Logger
 	// Cluster name, should be locally unique
 	Name string
 	// Cluster key, must be shared across nodes
diff --git a/pkg/cluster/env.go b/pkg/cluster/env.go
index 022da80600ca8aade81c273b21582e135bd60cf8..e25ea85cf77a923a5729c9f45312d58bd5d312dd 100644
--- a/pkg/cluster/env.go
+++ b/pkg/cluster/env.go
@@ -4,8 +4,6 @@ import (
 	"fmt"
 	"os"
 	"path"
-
-	"github.com/sirupsen/logrus"
 )
 
 const homeDir = "/root"
@@ -17,20 +15,24 @@ func (c *Cluster) initEnv() {
 	// Remove all binaries and configs
 	err := os.RemoveAll(binDir)
 	if err != nil {
-		logrus.Fatal(err)
+		c.settings.Logger.Error(err, "could not prepare bin dir")
+		os.Exit(1)
 	}
 	err = os.RemoveAll(homeDir)
 	if err != nil {
-		logrus.Fatal(err)
+		c.settings.Logger.Error(err, "could not prepare home dir")
+		os.Exit(1)
 	}
 	// Create bin directory and all useful symlinks
 	err = os.Setenv("PATH", binDir)
 	if err != nil {
-		logrus.Fatal(err)
+    c.settings.Logger.Error(err, "could not set binaries path")
+    os.Exit(1)
 	}
 	err = os.MkdirAll(binDir, 0o755)
 	if err != nil {
-		logrus.Fatal(err)
+    c.settings.Logger.Error(err, "could not prepare bin dir")
+    os.Exit(1)
 	}
 	for _, name := range []string{"kubectl", "ctr", "mount", "containerd-shim", "containerd-shim-runc-v2", "runc"} {
 		err = os.Symlink("/proc/1/exe", path.Join(binDir, name))
@@ -43,6 +45,6 @@ func (c *Cluster) initEnv() {
 			ClientKey:  c.masterCerts.RootClient.KeyPath(),
 			ClientCert: c.masterCerts.RootClient.CertPath(),
 		}
-		WriteConfig(rootConfig, path.Join(homeDir, ".kube/config"))
+		c.WriteConfig(rootConfig, path.Join(homeDir, ".kube/config"))
 	}
 }
diff --git a/pkg/cluster/kubeconfig.go b/pkg/cluster/kubeconfig.go
index c5c945c0a52634ebc0c8d506c55d8e6b935af339..88f8c8390c2d498f87bea4ddd1404898ea05a3fb 100644
--- a/pkg/cluster/kubeconfig.go
+++ b/pkg/cluster/kubeconfig.go
@@ -4,28 +4,29 @@ import (
 	"os"
   "path"
 	"text/template"
-
-  "github.com/sirupsen/logrus"
 )
 
 type WriteableConfig interface {
   Template() string
 }
 
-func WriteConfig(c WriteableConfig, dest string) {
-  t, _ := template.New("template").Parse(c.Template())
+func (c *Cluster) WriteConfig(config WriteableConfig, dest string) {
+  t, _ := template.New("template").Parse(config.Template())
   err := os.MkdirAll(path.Dir(dest), 0o755)
   if err != nil {
-    logrus.Fatal(err)
+    c.settings.Logger.Error(err, "could not create config dir")
+    os.Exit(1)
   }
   file, err := os.Create(dest)
   if err != nil {
-    logrus.Fatal(err)
+    c.settings.Logger.Error(err, "could not create config file")
+    os.Exit(1)
   }
   defer file.Close()
   err = t.Execute(file, c)
   if err != nil {
-    logrus.Fatal(err)
+    c.settings.Logger.Error(err, "could not generate config")
+    os.Exit(1)
   }
 }
 
diff --git a/pkg/cluster/log.go b/pkg/cluster/log.go
deleted file mode 100644
index a2beee00011071cfaf04388454d8540f19a6de68..0000000000000000000000000000000000000000
--- a/pkg/cluster/log.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package cluster
-
-import (
-	"github.com/bombsimon/logrusr/v3"
-	"github.com/sirupsen/logrus"
-	"k8s.io/klog/v2"
-)
-
-func SetupLogging() {
-	logrus.SetLevel(logrus.DebugLevel)
-	log := logrusr.New(logrus.StandardLogger())
-  klog.SetLogger(log)
-}
diff --git a/pkg/cluster/services.go b/pkg/cluster/services.go
index 784eb7eca3175a67b414160795fdecee7bb492d2..a54fc5c533484e8691e375df9ce6c309edf143a3 100644
--- a/pkg/cluster/services.go
+++ b/pkg/cluster/services.go
@@ -4,44 +4,26 @@ import (
 	"context"
 	"fmt"
 	"net"
-  "time"
+  "os"
+	"time"
 
-	"forge.tedomum.net/acides/hepto/hepto/pkg/pki"
 	"forge.tedomum.net/acides/hepto/hepto/pkg/wrappers"
-	"github.com/sirupsen/logrus"
 	"go.etcd.io/etcd/server/v3/embed"
 )
 
-type ClusterServices struct {
-	ctx     context.Context
-	started bool
-	done    chan struct{}
-}
-
-func NewClusterServices() *ClusterServices {
-	return &ClusterServices{
-		ctx:  context.Background(),
-		done: make(chan struct{}),
-	}
-}
-
-func (s *ClusterServices) watch(ctx context.Context) {
+func (c *Cluster) watchService(ctx context.Context) {
 	<-ctx.Done()
-	s.done <- struct{}{}
-}
-
-func (s *ClusterServices) Done() <-chan struct{} {
-	return s.done
+	c.servicesDone <- struct{}{}
 }
 
-func (s *ClusterServices) Update(c *Cluster) {
-	if s.started {
+func (c *Cluster) updateServices() {
+	if c.servicesStarted {
 		return
 	}
 	if c.node.Role == Master {
-		s.startEtcd()
-		s.startK8sMaster(c.networking, c.pki, c.masterCerts)
-		s.started = true
+		c.startEtcd()
+		c.startK8sMaster()
+		c.servicesStarted = true
 	} else if c.node.Role == Node {
 		// Bail if CA are not discovered or certificates are not ready
 		if c.pki.TLS.Cert == nil || c.pki.Kubelet.Cert == nil {
@@ -50,123 +32,129 @@ func (s *ClusterServices) Update(c *Cluster) {
 		if c.certs.TLS.Cert == nil || c.certs.API.Cert == nil {
 			return
 		}
-		logrus.Debug("certificates are...", c.pki.TLS.Cert, c.pki.API.Cert)
-		logrus.Debug("looking for master node...")
+		c.settings.Logger.Info("looking for master node")
 		for _, node := range c.ml.Nodes() {
 			if node.NodeMeta.Role == string(Master) {
-				s.startK8sNode(node.NodeMeta.VpnIP, c.pki, c.certs)
-				s.started = true
+				c.startK8sNode(node.NodeMeta.VpnIP)
+				c.servicesStarted = true
 			}
 		}
 	}
 }
 
-func (s *ClusterServices) startEtcd() {
+func (c *Cluster) startEtcd() {
 	etcdConfig := embed.NewConfig()
 	etcdConfig.Dir = "/etcd"
-	service, err := wrappers.ETCd(s.ctx, etcdConfig)
+	service, err := wrappers.ETCd(c.serviceCtx, etcdConfig)
 	if err != nil {
-		logrus.Fatal(err)
+    c.settings.Logger.Error(err, "failed to start etcd")
+    os.Exit(1)
 	}
-	go s.watch(service)
+	go c.watchService(service)
 }
 
-func (s *ClusterServices) startK8sMaster(net *ClusterNetworking, ca *pki.ClusterCA, certs *pki.MasterCerts) {
-	api, err := wrappers.APIServer(s.ctx, []string{
-		"--bind-address", net.NodeAddress.IP.String(),
-		"--service-cluster-ip-range", net.ServiceNet.String(),
-		"--tls-cert-file", certs.TLS.CertPath(),
-		"--tls-private-key-file", certs.TLS.KeyPath(),
-		"--client-ca-file", ca.API.CertPath(),
-		"--kubelet-certificate-authority", ca.Kubelet.CertPath(),
-		"--kubelet-client-certificate", certs.Kubelet.CertPath(),
-		"--kubelet-client-key", certs.Kubelet.KeyPath(),
+func (c *Cluster) startK8sMaster() {
+	api, err := wrappers.APIServer(c.serviceCtx, []string{
+		"--bind-address", c.networking.NodeAddress.IP.String(),
+		"--service-cluster-ip-range", c.networking.ServiceNet.String(),
+		"--tls-cert-file", c.masterCerts.TLS.CertPath(),
+		"--tls-private-key-file", c.masterCerts.TLS.KeyPath(),
+		"--client-ca-file", c.pki.API.CertPath(),
+		"--kubelet-certificate-authority", c.pki.Kubelet.CertPath(),
+		"--kubelet-client-certificate", c.masterCerts.Kubelet.CertPath(),
+		"--kubelet-client-key", c.masterCerts.Kubelet.KeyPath(),
 		"--etcd-servers", "http://localhost:2379",
-		"--service-account-signing-key-file", certs.Tokens.KeyPath(),
-		"--service-account-key-file", certs.Tokens.KeyPath(),
+		"--service-account-signing-key-file", c.masterCerts.Tokens.KeyPath(),
+		"--service-account-key-file", c.masterCerts.Tokens.KeyPath(),
 		"--service-account-issuer", "https://kubernetes.default.svc.cluster.local",
 		"--api-audiences", "https://kubernetes.default.svc.cluster.local",
 		"--authorization-mode", "Node,RBAC",
 	})
 	if err != nil {
-		logrus.Fatal(err)
+    c.settings.Logger.Error(err, "failed to start APIserver")
+    os.Exit(1)
 	}
 	cmConfig := KubeConfig{
-		URL:        fmt.Sprintf("https://[%s]:6443", net.NodeAddress.IP.String()),
-		CACert:     ca.TLS.CertPath(),
-		ClientCert: certs.ControllersAPI.CertPath(),
-		ClientKey:  certs.ControllersAPI.KeyPath(),
+		URL:        fmt.Sprintf("https://[%s]:6443", c.networking.NodeAddress.IP.String()),
+		CACert:     c.pki.TLS.CertPath(),
+		ClientCert: c.masterCerts.ControllersAPI.CertPath(),
+		ClientKey:  c.masterCerts.ControllersAPI.KeyPath(),
 	}
 	cmConfigPath := "/etc/k8s/controller-manager.yaml"
-  WriteConfig(cmConfig, cmConfigPath)
-	cm, err := wrappers.ControllerManager(s.ctx, []string{
+  c.WriteConfig(cmConfig, cmConfigPath)
+	cm, err := wrappers.ControllerManager(c.serviceCtx, []string{
 		"--kubeconfig", cmConfigPath,
-		"--tls-cert-file", certs.ControllersTLS.CertPath(),
-		"--tls-private-key-file", certs.ControllersTLS.KeyPath(),
-		"--service-account-private-key-file", certs.Tokens.KeyPath(),
+		"--tls-cert-file", c.masterCerts.ControllersTLS.CertPath(),
+		"--tls-private-key-file", c.masterCerts.ControllersTLS.KeyPath(),
+		"--service-account-private-key-file", c.masterCerts.Tokens.KeyPath(),
 		"--use-service-account-credentials",
 	})
 	if err != nil {
-		logrus.Fatal(err)
+    c.settings.Logger.Error(err, "failed to start controller manager")
+    os.Exit(1)
 	}
 	schedulerConfig := KubeConfig{
-		URL:        fmt.Sprintf("https://[%s]:6443", net.NodeAddress.IP.String()),
-		CACert:     ca.TLS.CertPath(),
-		ClientCert: certs.SchedulerAPI.CertPath(),
-		ClientKey:  certs.SchedulerAPI.KeyPath(),
+		URL:        fmt.Sprintf("https://[%s]:6443", c.networking.NodeAddress.IP.String()),
+		CACert:     c.pki.TLS.CertPath(),
+		ClientCert: c.masterCerts.SchedulerAPI.CertPath(),
+		ClientKey:  c.masterCerts.SchedulerAPI.KeyPath(),
 	}
 	schedulerConfigPath := "/etc/k8s/scheduler.yaml"
-	WriteConfig(schedulerConfig, schedulerConfigPath)
-	scheduler, err := wrappers.Scheduler(s.ctx, []string{
+	c.WriteConfig(schedulerConfig, schedulerConfigPath)
+	scheduler, err := wrappers.Scheduler(c.serviceCtx, []string{
 		"--kubeconfig", schedulerConfigPath,
 	})
 	if err != nil {
-		logrus.Fatal(err)
+    c.settings.Logger.Error(err, "failed to start scheduler")
+    os.Exit(1)
 	}
-	go s.watch(api)
-	go s.watch(cm)
-	go s.watch(scheduler)
+	go c.watchService(api)
+	go c.watchService(cm)
+	go c.watchService(scheduler)
 }
 
-func (s *ClusterServices) startK8sNode(master net.IP, ca *pki.ClusterCA, certs *pki.NodeCerts) {
+func (c *Cluster) startK8sNode(masterIP net.IP) {
 	// Containerd
 	containerdConfig := ContainerdConfig{
 		RootDir: "/containerd",
 		Socket:  "/containerd.sock",
 	}
 	containerdConfigPath := "/etc/containerd/config.toml"
-	WriteConfig(containerdConfig, containerdConfigPath)
-	containerd, err := wrappers.Containerd(s.ctx, []string{
+	c.WriteConfig(containerdConfig, containerdConfigPath)
+	containerd, err := wrappers.Containerd(c.serviceCtx, []string{
 		"--config", containerdConfigPath,
 	})
 	if err != nil {
-		logrus.Fatal("could not start containerd:", err)
+    c.settings.Logger.Error(err, "failed to start containerd")
+    os.Exit(1)
 	}
+  // Wait for containerd to settle down
   time.Sleep(5 * time.Second)
 	// Kubelet
 	kubeletKubeConfig := KubeConfig{
-		URL:        fmt.Sprintf("https://[%s]:6443", master.String()),
-		CACert:     ca.TLS.CertPath(),
-		ClientCert: certs.API.CertPath(),
-		ClientKey:  certs.API.KeyPath(),
+		URL:        fmt.Sprintf("https://[%s]:6443", masterIP.String()),
+		CACert:     c.pki.TLS.CertPath(),
+		ClientCert: c.certs.API.CertPath(),
+		ClientKey:  c.certs.API.KeyPath(),
 	}
 	kubeletKubeConfigPath := "/etc/k8s/kubelet-kubeconfig.yaml"
-	WriteConfig(kubeletKubeConfig, kubeletKubeConfigPath)
+	c.WriteConfig(kubeletKubeConfig, kubeletKubeConfigPath)
 	kubeletConfig := KubeletConfig{
-		CACert:  ca.Kubelet.CertPath(),
-		TLSCert: certs.TLS.CertPath(),
-		TLSKey:  certs.TLS.KeyPath(),
+		CACert:  c.pki.Kubelet.CertPath(),
+		TLSCert: c.certs.TLS.CertPath(),
+		TLSKey:  c.certs.TLS.KeyPath(),
 	}
 	kubeletConfigPath := "/etc/k8s/kubelet.yaml"
-	WriteConfig(kubeletConfig, kubeletConfigPath)
-	kubelet, err := wrappers.Kubelet(s.ctx, []string{
+	c.WriteConfig(kubeletConfig, kubeletConfigPath)
+	kubelet, err := wrappers.Kubelet(c.serviceCtx, []string{
 		"--kubeconfig", kubeletKubeConfigPath,
 		"--config", kubeletConfigPath,
 		"--container-runtime-endpoint", "unix://" + containerdConfig.Socket,
 	})
 	if err != nil {
-		logrus.Fatal(err)
+    c.settings.Logger.Error(err, "failed to start kubelet")
+    os.Exit(1)
 	}
-	go s.watch(containerd)
-	go s.watch(kubelet)
+	go c.watchService(containerd)
+	go c.watchService(kubelet)
 }
diff --git a/pkg/cluster/vpn.go b/pkg/cluster/vpn.go
index f01e9c6c39e37e7b0dbbe632535bcfb5b26b1638..998de64fecd878e6b6c3b5b977dbacfa08b1ed5c 100644
--- a/pkg/cluster/vpn.go
+++ b/pkg/cluster/vpn.go
@@ -2,16 +2,17 @@ package cluster
 
 import (
 	"net"
+  "os"
 
 	"forge.tedomum.net/acides/hepto/hepto/pkg/wg"
-	"github.com/sirupsen/logrus"
 )
 
 func (c *Cluster) initVPN() {
 	// Prepare wireguard
-	vpn, err := wg.New("wg0", 7124, c.networking.NodeAddress)
+	vpn, err := wg.New(c.settings.Logger, "wg0", 7124, c.networking.NodeAddress)
 	if err != nil {
-		logrus.Fatal("could not initialize wireguard: ", err)
+		c.settings.Logger.Error(err, "could not initialize wireguard")
+    os.Exit(1)
 	}
 	c.vpn = vpn
 	c.ml.Meta.VpnIP = vpn.IP()
@@ -28,16 +29,16 @@ func (c *Cluster) updateVPN() {
 		// Check whterher the node ip is consistent
 		peerAddr := DeriveAddress(c.networking.NodeNet, node.Name).IP
 		if !meta.VpnIP.Equal(peerAddr) {
-			logrus.Error("inconsistent node IP for ", node.Name)
+			c.settings.Logger.Info("inconsistent node IP", "name", node.Name)
 			continue
 		}
 		peer, err := c.vpn.MakePeer(node.Addr, meta.VpnKey, peerAddr, []net.IPNet{})
 		if err != nil {
-			logrus.Warn("cannot setup VPN with node ", node.Name, ": ", err)
+      c.settings.Logger.Error(err, "cannot setup VPN", "name", node.Name)
 			continue
 		}
 		peers = append(peers, peer)
 	}
-	logrus.Debugf("updating VPN mesh, %d peers, MTU %d", len(peers), c.networking.MTU)
+	c.settings.Logger.Info("updating VPN mesh", peers, len(peers), "mtu", c.networking.MTU)
 	c.vpn.Update(peers, c.networking.MTU)
 }
diff --git a/pkg/pki/utils.go b/pkg/pki/utils.go
index 7fe80b40700af873ecca6ee9ab28c757b61e9fe8..ca52dfbaff4318a1da9a69ecc1633e9c0f923dab 100644
--- a/pkg/pki/utils.go
+++ b/pkg/pki/utils.go
@@ -4,7 +4,6 @@ import (
 	"crypto/x509"
 
 	"forge.tedomum.net/acides/hepto/hepto/pkg/pekahi"
-	"github.com/sirupsen/logrus"
 )
 
 // Merge a single node or master certificate
@@ -30,10 +29,6 @@ func mergeCert(local *pekahi.Certificate, remote *pekahi.Certificate) bool {
 
 func signCert(ca *pekahi.Certificate, c *pekahi.Certificate, template *x509.Certificate) {
 	if c.CSR != nil && c.Cert == nil {
-		logrus.Info("signing certificate ", c.CSR.Subject.String())
-		err := ca.Sign(c, template)
-		if err != nil {
-			logrus.Warnf("cannot sign certificate for %s: %s", c.CSR.Subject.String(), err)
-		}
+		ca.Sign(c, template)
 	}
 }
diff --git a/pkg/selfcontain/config.go b/pkg/selfcontain/config.go
index dd6b44cb1510150f4398fe2ece16f30504d59cd6..94f419c1d09390523638222028313b28d16be702 100644
--- a/pkg/selfcontain/config.go
+++ b/pkg/selfcontain/config.go
@@ -3,12 +3,15 @@ package selfcontain
 import (
 	"net"
 
+	"github.com/go-logr/logr"
 	"github.com/opencontainers/runc/libcontainer/configs"
 	"github.com/opencontainers/runc/libcontainer/devices"
 	"golang.org/x/sys/unix"
 )
 
 type Config struct {
+  // Logger interface
+  Logger logr.Logger
 	// Container name
 	Name string
 	// Path to on-file container data
diff --git a/pkg/selfcontain/container.go b/pkg/selfcontain/container.go
index effa8a2aa5371067cbd23d14e86119def3d52d5e..c5b56500d5e8b03cf20599a5dfb127e0567c8158 100644
--- a/pkg/selfcontain/container.go
+++ b/pkg/selfcontain/container.go
@@ -13,7 +13,6 @@ import (
 	"github.com/opencontainers/runc/libcontainer"
 	"github.com/opencontainers/runc/libcontainer/configs"
 	_ "github.com/opencontainers/runc/libcontainer/nsenter"
-	"github.com/sirupsen/logrus"
 )
 
 type Container struct {
@@ -69,8 +68,7 @@ func (c *Container) Start(args []string) error {
 		c.container.Destroy()
 		return err
 	}
-	logrus.Debug("running with pid ", os.Getpid())
-	logrus.Debug("namespaced with pid ", pid)
+	c.config.Logger.Info("container started", "parent", os.Getpid(), "container", pid)
 	c.process = process
 	return nil
 }
@@ -87,15 +85,15 @@ func (c *Container) Run() error {
 }
 
 func (c *Container) Destroy() error {
-	logrus.Info("destroying container ", c.config.Name)
+	c.config.Logger.Info("destroying container", "name", c.config.Name)
 	err := c.container.Destroy()
 	if err != nil {
-		logrus.Error(err)
+    c.config.Logger.Error(err, "could not destroy")
 		return err
 	}
 	err = os.RemoveAll(filepath.Join(c.config.Data, c.config.Name))
 	if err != nil {
-		logrus.Error(err)
+    c.config.Logger.Error(err, "could not cleanup")
 		return err
 	}
 	return nil
diff --git a/pkg/selfcontain/init.go b/pkg/selfcontain/init.go
index eeb2a64780eb417146aea51236218cd1e7de5d5e..827b7112cfe5362a58f8904bb82d98e05dfb1401 100644
--- a/pkg/selfcontain/init.go
+++ b/pkg/selfcontain/init.go
@@ -1,11 +1,11 @@
 package selfcontain
 
 import (
+  "fmt"
 	"os"
 	"runtime"
 
 	"github.com/opencontainers/runc/libcontainer"
-	"github.com/sirupsen/logrus"
 )
 
 // libcontainer uses a three-step containerization technique:
@@ -27,11 +27,11 @@ func init() {
 		runtime.LockOSThread()
 		// Run libcontainer initialization, which will fork/exec to the
 		// provided process executable, a.k.a ourselves
-		logrus.Debug("initializing self-contained app")
 		factory, _ := libcontainer.New("")
 		err := factory.StartInitialization()
 		if err != nil {
-			logrus.Fatal("could not run self-contained app: ", err)
+      fmt.Printf("could not initialize app: %v", err)
+      os.Exit(1)
 		}
 	}
 }
diff --git a/pkg/selfcontain/utils.go b/pkg/selfcontain/utils.go
index 1680957b8a1ec3bd36675d4fad753e345ddc8e46..37cb82cbe69e58df7d1593047d9945f27df0bddf 100644
--- a/pkg/selfcontain/utils.go
+++ b/pkg/selfcontain/utils.go
@@ -3,8 +3,6 @@ package selfcontain
 import (
 	"os"
 	"os/signal"
-
-	"github.com/sirupsen/logrus"
 )
 
 type runnable func()
@@ -15,26 +13,30 @@ func RunFun(config *Config, f runnable) error {
 	// Run the function if we are indeed inside the container
 	for _, arg := range os.Args {
 		if arg == argRunFun {
-			logrus.Debug("we are now running inside the container")
-			Evacuate()
-			logrus.Debug("starting the main routine")
+			config.Logger.Info("now running inside the container")
+      err := Evacuate()
+      if err != nil {
+        config.Logger.Error(err, "could not evacuate")
+        os.Exit(1)
+      }
+			config.Logger.Info("starting the main routine")
 			f()
 			return nil
 		}
 	}
 	// Otherwise containerize ourselves
-	logrus.Debug("setting up a new container")
+	config.Logger.Info("setting up a new container")
 	c, err := New(config)
 	if err != nil {
 		return err
 	}
 	defer c.Destroy()
-	logrus.Debug("starting the container init process")
+  config.Logger.Info("starting the container init process")
 	err = c.Start(append(os.Args, argRunFun))
 	if err != nil {
 		return err
 	}
-	logrus.Debug("setting up container networking")
+	config.Logger.Info("setting up container networking")
 	err = c.SetupNetworking()
 	if err != nil {
 		return err
@@ -45,27 +47,24 @@ func RunFun(config *Config, f runnable) error {
 	signal.Notify(s, os.Interrupt)
 	go func() {
 		<-s
-		logrus.Debug("interrupt signal caught, tearing down")
+		config.Logger.Info("interrupt signal caught, tearing down")
 		c.Destroy()
 	}()
-	logrus.Debug("container is ready, handing over")
+	config.Logger.Info("container is ready, handing over")
 	return c.Run()
 }
 
 // Evacuate cgroups, which is required for many in-container use cases
 // Remaining in the root cgroup would prevent creating any domain sub-cgroup
-func Evacuate() {
+func Evacuate() error {
 	// Libcontainer cgroup manager is not designed for evacuation and will
 	// fail in such a case, so we are using cgroupfs directly, which is
 	// explicitely available due to defaults, and simple since we are
 	// the only running process at the moment
-	logrus.Debug("evacuating self to /selfcontain")
 	err := os.Mkdir("/sys/fs/cgroup/selfcontain", 0o755)
 	if err != nil && !os.IsExist(err) {
-		logrus.Fatal("could not create evacuation cgroup: ", err)
+    return err
 	}
 	err = os.WriteFile("/sys/fs/cgroup/selfcontain/cgroup.procs", []byte("0"), 0o755)
-	if err != nil {
-		logrus.Fatal("could not evacuate self: ", err)
-	}
+  return err
 }
diff --git a/pkg/sml/delegate.go b/pkg/sml/delegate.go
index e2e17cb3e1da77f9d1e1d308878bd1193e478489..8e332a6f7884889f7f611eb762951ac93b1fccd7 100644
--- a/pkg/sml/delegate.go
+++ b/pkg/sml/delegate.go
@@ -2,7 +2,6 @@ package sml
 
 import (
 	"github.com/hashicorp/memberlist"
-	"github.com/sirupsen/logrus"
 )
 
 // Cluster implements the memberlist.Delegate interface
@@ -13,7 +12,7 @@ func (m *Memberlist[M, S, MP, SP]) NotifyConflict(node, other *memberlist.Node)
 func (m *Memberlist[M, S, MP, SP]) NodeMeta(limit int) []byte {
 	n, err := m.Meta.Encode()
 	if err != nil {
-		logrus.Warn("could not encode node metadata")
+		m.logger.Info("could not encode node metadata")
 		return []byte{}
 	}
 	return n
@@ -32,7 +31,7 @@ func (m *Memberlist[M, S, MP, SP]) GetBroadcasts(overhead, limit int) [][]byte {
 func (m *Memberlist[M, S, MP, SP]) LocalState(join bool) []byte {
 	s, err := m.State.Encode()
 	if err != nil {
-		logrus.Warn("could not encode local state")
+		m.logger.Info("could not encode local state")
 		return []byte{}
 	}
 	return s
@@ -40,20 +39,19 @@ func (m *Memberlist[M, S, MP, SP]) LocalState(join bool) []byte {
 
 // Clutser implements the memberlist.Delegate interface
 func (m *Memberlist[M, S, MP, SP]) MergeRemoteState(buf []byte, join bool) {
-	logrus.Debug("merging remote state")
+	m.logger.Info("merging remote state")
 	change, err := m.State.Merge(buf)
 	if err != nil {
-		logrus.Warn("could not merge remote state:", err)
+		m.logger.Error(err, "could not merge remote state")
 	}
 	if change {
 		m.nodeChanges <- struct{}{}
 	}
-	logrus.Debug(m.State.String())
 }
 
 // Cluster implements the EventDelegate interface
 func (m *Memberlist[M, S, MP, SP]) NotifyJoin(n *memberlist.Node) {
-	logrus.Debug("node joined: ", n.Name)
+  m.logger.Info("node joined", "name", n.Name)
 	m.nodeChanges <- struct{}{}
 }
 
diff --git a/pkg/sml/memberlist.go b/pkg/sml/memberlist.go
index 456135ae22a996e14f9d98343b8b64a82661b3dd..553e0570894bb78df481d1d43f2085924381c028 100644
--- a/pkg/sml/memberlist.go
+++ b/pkg/sml/memberlist.go
@@ -9,8 +9,8 @@ import (
 	"net"
 	"time"
 
+	"github.com/go-logr/logr"
 	"github.com/hashicorp/memberlist"
-	"github.com/sirupsen/logrus"
 )
 
 type Memberlist[M any, S any, MP MetaPointer[M], SP StatePointer[S]] struct {
@@ -20,6 +20,7 @@ type Memberlist[M any, S any, MP MetaPointer[M], SP StatePointer[S]] struct {
 	nodeName    string
 	anchors     []string
 	key         []byte
+	logger      logr.Logger
 	config      *memberlist.Config
 	ml          *memberlist.Memberlist
 	nodeCache   []Node[M, MP]
@@ -28,9 +29,18 @@ type Memberlist[M any, S any, MP MetaPointer[M], SP StatePointer[S]] struct {
 	transport   *instrumentedTransport
 }
 
-func New[M any, S any, MP MetaPointer[M], SP StatePointer[S]](nodeName string, nodeIP net.IP, port int, anchors []string, key []byte) *Memberlist[M, S, MP, SP] {
+type logWriter struct {
+	logr.Logger
+}
+
+func (w logWriter) Write(b []byte) (int, error) {
+	w.Info(string(b))
+	return len(b), nil
+}
+
+func New[M any, S any, MP MetaPointer[M], SP StatePointer[S]](nodeName string, nodeIP net.IP, port int, anchors []string, key []byte, logger logr.Logger) *Memberlist[M, S, MP, SP] {
 	config := memberlist.DefaultLANConfig()
-	config.LogOutput = logrus.StandardLogger().WriterLevel(logrus.DebugLevel)
+	config.LogOutput = logWriter{logger}
 	config.SecretKey = key
 	config.BindAddr = nodeIP.String()
 	config.BindPort = port
@@ -44,6 +54,7 @@ func New[M any, S any, MP MetaPointer[M], SP StatePointer[S]](nodeName string, n
 		nodeChanges: make(chan struct{}, 100),
 		anchors:     anchors,
 		key:         key,
+		logger:      logger,
 		config:      config,
 		Meta:        new(M),
 		State:       new(S),
@@ -59,12 +70,12 @@ func New[M any, S any, MP MetaPointer[M], SP StatePointer[S]](nodeName string, n
 
 // Start the memberlist cluster by listening on main sockets
 func (m *Memberlist[M, S, MP, SP]) Start() error {
-	logrus.Info("starting the cluster transport")
+	m.logger.Info("starting the cluster transport")
 	tc := &memberlist.NetTransportConfig{
 		BindAddrs: []string{m.config.BindAddr},
 		BindPort:  m.config.BindPort,
 	}
-	transport, err := NewTransport(tc)
+	transport, err := NewTransport(tc, m.logger)
 	if err != nil {
 		return err
 	}
@@ -88,11 +99,8 @@ func (m *Memberlist[M, S, MP, SP]) Run() error {
 		case <-ticker:
 			go m.join()
 		case <-m.nodeChanges:
-			logrus.Debug("network topology changed, a node just joined, left or was updated")
 			m.updateCache()
-			for _, node := range m.Nodes() {
-				logrus.Debugf("* %s [%s]\n", node.Name, node.NodeMeta.String())
-			}
+			m.logger.Info("network topology changed", "nodes", m.Nodes())
 			for _, c := range m.chans {
 				c <- struct{}{}
 			}
@@ -121,7 +129,7 @@ func (m *Memberlist[M, S, MP, SP]) Instr() Instrumentation {
 
 // Update the current node
 func (m *Memberlist[M, S, MP, SP]) Update() {
-	logrus.Debug("updating the memberlist cluster")
+	m.logger.Info("updating the memberlist cluster")
 	m.ml.UpdateNode(1 * time.Second)
 }
 
@@ -129,7 +137,7 @@ func (m *Memberlist[M, S, MP, SP]) Update() {
 // nodes and decodes metadata
 func (m *Memberlist[M, S, MP, SP]) updateCache() {
 	members := m.ml.Members()
-	logrus.Debugf("updating the node cache with %d members", len(members))
+	m.logger.Info("updating the node cache", "count", len(members))
 	var cache []Node[M, MP]
 	for _, mlNode := range members {
 		meta := new(M)
@@ -138,7 +146,7 @@ func (m *Memberlist[M, S, MP, SP]) updateCache() {
 		if err == nil {
 			cache = append(cache, Node[M, MP]{mlNode, pointer})
 		} else {
-			logrus.Warnf("could not decode meta for node %s", mlNode.Name)
+			m.logger.Info("could not decode meta", "node", mlNode.Name)
 		}
 	}
 	m.nodeCache = cache
@@ -162,7 +170,7 @@ func (m *Memberlist[M, S, MP, SP]) join() error {
 		addrs = append(addrs, candidate)
 	}
 	if len(addrs) > 0 {
-		logrus.Info("joining cluster nodes: ", addrs)
+		m.logger.Info("joining cluster nodes", "addresses", addrs)
 	}
 	_, err := m.ml.Join(addrs)
 	return err
diff --git a/pkg/sml/transport.go b/pkg/sml/transport.go
index c0361b7b763630afb75a5abb727f031baab733e8..a1dd6c91d4f0ad91b0197582057a4341022b522b 100644
--- a/pkg/sml/transport.go
+++ b/pkg/sml/transport.go
@@ -4,8 +4,8 @@ import (
 	"net"
 	"time"
 
+	"github.com/go-logr/logr"
 	"github.com/hashicorp/memberlist"
-	"github.com/sirupsen/logrus"
 	"golang.org/x/net/ipv6"
 )
 
@@ -19,10 +19,12 @@ type instrumentedTransport struct {
 	minPmtu int
 	// Instrumentation updates
 	updates chan struct{}
+  // Logger interface
+  logger logr.Logger
 }
 
 // Create a new instrumented transport
-func NewTransport(config *memberlist.NetTransportConfig) (*instrumentedTransport, error) {
+func NewTransport(config *memberlist.NetTransportConfig, logger logr.Logger) (*instrumentedTransport, error) {
 	nt, err := memberlist.NewNetTransport(config)
 	if err != nil {
 		return nil, err
@@ -32,6 +34,7 @@ func NewTransport(config *memberlist.NetTransportConfig) (*instrumentedTransport
 		make(map[string]int),
 		1500,
 		make(chan struct{}, 100),
+    logger,
 	}, nil
 }
 
@@ -39,24 +42,24 @@ func NewTransport(config *memberlist.NetTransportConfig) (*instrumentedTransport
 func (t *instrumentedTransport) DialAddressTimeout(a memberlist.Address, timeout time.Duration) (net.Conn, error) {
 	addr := a.Addr
 
-	logrus.Debugf("memberlist dialing %s", addr)
+	t.logger.Info("memberlist dialing", "address", addr)
 	dialer := net.Dialer{Timeout: timeout}
 	conn, err := dialer.Dial("tcp", addr)
 	if err == nil {
 		wrapped := ipv6.NewConn(conn)
 		mtu, err := wrapped.PathMTU()
 		if err == nil {
-			logrus.Debugf("node %s has MTU %d", addr, mtu)
+      t.logger.Info("discovered MTU", "node", addr, "mtu", mtu)
 			prev, _ := t.pmtu[addr]
 			t.pmtu[addr] = mtu
 			if prev != mtu {
 				t.updateMinMTU()
 			}
 		} else {
-			logrus.Debug(err)
+			t.logger.Error(err, "could not discover MTU")
 		}
 	} else {
-		logrus.Debug(err)
+		t.logger.Error(err, "could not connect to remote")
 	}
 	return conn, err
 }
diff --git a/pkg/wg/wireguard.go b/pkg/wg/wireguard.go
index 64f672954812d8579f4f6232747b26cd9f0e620e..b484fb60ce4d89014e571b224ae4f54c5ff5587d 100644
--- a/pkg/wg/wireguard.go
+++ b/pkg/wg/wireguard.go
@@ -5,8 +5,8 @@ import (
 	"os"
 	"time"
 
+	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
-	"github.com/sirupsen/logrus"
 	"github.com/vishvananda/netlink"
 	"golang.zx2c4.com/wireguard/wgctrl"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -16,6 +16,7 @@ type Peer wgtypes.PeerConfig
 
 // Wireguard interface config
 type Wireguard struct {
+	logger    logr.Logger
 	iface     string
 	ipnet     *net.IPNet
 	client    *wgctrl.Client
@@ -25,7 +26,7 @@ type Wireguard struct {
 	PubKey    wgtypes.Key
 }
 
-func New(iface string, port int, ipnet *net.IPNet) (*Wireguard, error) {
+func New(logger logr.Logger, iface string, port int, ipnet *net.IPNet) (*Wireguard, error) {
 	client, err := wgctrl.New()
 	if err != nil {
 		return nil, errors.Wrap(err, "could not instantiate wireguard client")
@@ -38,6 +39,7 @@ func New(iface string, port int, ipnet *net.IPNet) (*Wireguard, error) {
 	pubKey := privKey.PublicKey()
 
 	return &Wireguard{
+		logger:    logger,
 		iface:     iface,
 		ipnet:     ipnet,
 		client:    client,
@@ -80,7 +82,7 @@ func (w *Wireguard) Update(peers []Peer, mtu int) error {
 	if err != nil {
 		return err
 	}
-	logrus.Debug("adding VPN address ", w.ipnet.String())
+	w.logger.Info("adding VPN address", "address", w.ipnet.String())
 	err = netlink.AddrAdd(link, &netlink.Addr{
 		IPNet: w.ipnet,
 	})
@@ -102,7 +104,7 @@ func (w *Wireguard) Update(peers []Peer, mtu int) error {
 		}
 		var gw net.IP
 		for _, route := range peer.AllowedIPs {
-			logrus.Debug("adding route to ", route.String())
+			w.logger.Info("new route", "dest", route.String())
 			scope := netlink.SCOPE_UNIVERSE
 			if overlay.Contains(route.IP) {
 				gw = route.IP