diff --git a/.golangci.yml b/.golangci.yml
index 8202fea..9893ff7 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -7,6 +7,7 @@ linters:
     - containedctx
     - contextcheck
     - cyclop
+    - depguard
     - exhaustivestruct
     - exhaustruct
     - forbidigo
@@ -22,6 +23,7 @@ linters:
     - interfacer
     - ireturn
     - lll
+    - musttag
     - nestif
     - nilnil
     - nlreturn
diff --git a/api/database.go b/api/database.go
index 554e7f4..3e1c825 100644
--- a/api/database.go
+++ b/api/database.go
@@ -52,7 +52,7 @@ func init() {
 	))
 }
 
-// DatabaseAPI is a database API instance.
+// DatabaseAPI is a generic database API interface.
 type DatabaseAPI struct {
 	queriesLock sync.Mutex
 	queries     map[string]*iterator.Iterator
@@ -67,6 +67,7 @@ type DatabaseAPI struct {
 	sendBytes func(data []byte)
 }
 
+// DatabaseWebsocketAPI is a database websocket API interface.
 type DatabaseWebsocketAPI struct {
 	DatabaseAPI
 
@@ -78,6 +79,7 @@ func allowAnyOrigin(r *http.Request) bool {
 	return true
 }
 
+// CreateDatabaseAPI creates a new database interface.
 func CreateDatabaseAPI(sendFunction func(data []byte)) DatabaseAPI {
 	return DatabaseAPI{
 		queries:        make(map[string]*iterator.Iterator),
@@ -195,6 +197,7 @@ func (api *DatabaseWebsocketAPI) shutdown(err error) error {
 	return nil
 }
 
+// Handle handles a message for the database API.
 func (api *DatabaseAPI) Handle(msg []byte) {
 	// 123|get|<key>
 	//    123|ok|<key>|<data>
@@ -370,7 +373,7 @@ func (api *DatabaseAPI) processQuery(opID []byte, q *query.Query) (ok bool) {
 		case <-api.shutdownSignal:
 			// cancel query and return
 			it.Cancel()
-			return
+			return false
 		case r := <-it.Next:
 			// process query feed
 			if r != nil {
@@ -656,7 +659,7 @@ func (api *DatabaseAPI) handleDelete(opID []byte, key string) {
 	api.send(opID, dbMsgTypeSuccess, emptyString, nil)
 }
 
-// MarshalRecords locks and marshals the given record, additionally adding
+// MarshalRecord locks and marshals the given record, additionally adding
 // metadata and returning it as json.
 func MarshalRecord(r record.Record, withDSDIdentifier bool) ([]byte, error) {
 	r.Lock()
diff --git a/api/endpoints.go b/api/endpoints.go
index 65de1b1..c60a602 100644
--- a/api/endpoints.go
+++ b/api/endpoints.go
@@ -208,7 +208,7 @@ func getAPIContext(r *http.Request) (apiEndpoint *Endpoint, apiRequest *Request)
 // does not pass the sanity checks.
 func RegisterEndpoint(e Endpoint) error {
 	if err := e.check(); err != nil {
-		return fmt.Errorf("%w: %s", ErrInvalidEndpoint, err)
+		return fmt.Errorf("%w: %w", ErrInvalidEndpoint, err)
 	}
 
 	endpointsLock.Lock()
@@ -224,6 +224,7 @@ func RegisterEndpoint(e Endpoint) error {
 	return nil
 }
 
+// GetEndpointByPath returns the endpoint registered with the given path.
 func GetEndpointByPath(path string) (*Endpoint, error) {
 	endpointsLock.Lock()
 	defer endpointsLock.Unlock()
diff --git a/api/request.go b/api/request.go
index 9891d9b..8a4d70b 100644
--- a/api/request.go
+++ b/api/request.go
@@ -33,6 +33,7 @@ type Request struct {
 // apiRequestContextKey is a key used for the context key/value storage.
 type apiRequestContextKey struct{}
 
+// RequestContextKey is the key used to add the API request to the context.
 var RequestContextKey = apiRequestContextKey{}
 
 // GetAPIRequest returns the API Request of the given http request.
diff --git a/database/interface_cache.go b/database/interface_cache.go
index cd57011..61d8cdf 100644
--- a/database/interface_cache.go
+++ b/database/interface_cache.go
@@ -45,7 +45,7 @@ func (i *Interface) DelayedCacheWriter(ctx context.Context) error {
 			i.flushWriteCache(0)
 
 		case <-thresholdWriteTicker.C:
-			// Often check if the the write cache has filled up to a certain degree and
+			// Often check if the write cache has filled up to a certain degree and
 			// flush it to storage before we start evicting to-be-written entries and
 			// slow down the hot path again.
 			i.flushWriteCache(percentThreshold)
diff --git a/database/storage/sinkhole/sinkhole.go b/database/storage/sinkhole/sinkhole.go
index e61c031..565c905 100644
--- a/database/storage/sinkhole/sinkhole.go
+++ b/database/storage/sinkhole/sinkhole.go
@@ -62,7 +62,7 @@ func (s *Sinkhole) PutMany(shadowDelete bool) (chan<- record.Record, <-chan erro
 	// start handler
 	go func() {
 		for range batch {
-			// nom, nom, nom
+			// discard everything
 		}
 		errs <- nil
 	}()
diff --git a/modules/modules.go b/modules/modules.go
index fe10e87..ebae2d0 100644
--- a/modules/modules.go
+++ b/modules/modules.go
@@ -30,7 +30,7 @@ var (
 )
 
 // Module represents a module.
-type Module struct {
+type Module struct { //nolint:maligned
 	sync.RWMutex
 
 	Name string
diff --git a/rng/entropy.go b/rng/entropy.go
index 7a2545d..488f3ee 100644
--- a/rng/entropy.go
+++ b/rng/entropy.go
@@ -44,7 +44,7 @@ func (f *Feeder) NeedsEntropy() bool {
 	return f.needsEntropy.IsSet()
 }
 
-// SupplyEntropy supplies entropy to to the Feeder, it will block until the Feeder has read from it.
+// SupplyEntropy supplies entropy to the Feeder, it will block until the Feeder has read from it.
 func (f *Feeder) SupplyEntropy(data []byte, entropy int) {
 	f.input <- &entropyData{
 		data:    data,
@@ -52,7 +52,7 @@ func (f *Feeder) SupplyEntropy(data []byte, entropy int) {
 	}
 }
 
-// SupplyEntropyIfNeeded supplies entropy to to the Feeder, but will not block if no entropy is currently needed.
+// SupplyEntropyIfNeeded supplies entropy to the Feeder, but will not block if no entropy is currently needed.
 func (f *Feeder) SupplyEntropyIfNeeded(data []byte, entropy int) {
 	if f.needsEntropy.IsSet() {
 		return
@@ -67,14 +67,14 @@ func (f *Feeder) SupplyEntropyIfNeeded(data []byte, entropy int) {
 	}
 }
 
-// SupplyEntropyAsInt supplies entropy to to the Feeder, it will block until the Feeder has read from it.
+// SupplyEntropyAsInt supplies entropy to the Feeder, it will block until the Feeder has read from it.
 func (f *Feeder) SupplyEntropyAsInt(n int64, entropy int) {
 	b := make([]byte, 8)
 	binary.LittleEndian.PutUint64(b, uint64(n))
 	f.SupplyEntropy(b, entropy)
 }
 
-// SupplyEntropyAsIntIfNeeded supplies entropy to to the Feeder, but will not block if no entropy is currently needed.
+// SupplyEntropyAsIntIfNeeded supplies entropy to the Feeder, but will not block if no entropy is currently needed.
 func (f *Feeder) SupplyEntropyAsIntIfNeeded(n int64, entropy int) {
 	if f.needsEntropy.IsSet() { // avoid allocating a slice if possible
 		b := make([]byte, 8)
diff --git a/utils/renameio/example_test.go b/utils/renameio/example_test.go
index 685e79e..7a2c28d 100644
--- a/utils/renameio/example_test.go
+++ b/utils/renameio/example_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/safing/portbase/utils/renameio"
 )
 
-func ExampleTempFile_justone() {
+func ExampleTempFile_justone() { //nolint:testableexamples
 	persist := func(temperature float64) error {
 		t, err := renameio.TempFile("", "/srv/www/metrics.txt")
 		if err != nil {
@@ -28,7 +28,7 @@ func ExampleTempFile_justone() {
 	}
 }
 
-func ExampleTempFile_many() {
+func ExampleTempFile_many() { //nolint:testableexamples
 	// Prepare for writing files to /srv/www, effectively caching calls to
 	// TempDir which TempFile would otherwise need to make.
 	dir := renameio.TempDir("/srv/www")
diff --git a/utils/structure_test.go b/utils/structure_test.go
index 0c277af..2acfebd 100644
--- a/utils/structure_test.go
+++ b/utils/structure_test.go
@@ -1,4 +1,4 @@
-// go:build !windows
+//go:build !windows
 
 package utils