Skip to content

Commit

Permalink
feat: add support for systemd socket activation
Browse files Browse the repository at this point in the history
  • Loading branch information
rgl committed Feb 16, 2024
1 parent 0aa6bf0 commit 43ca3c7
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 26 deletions.
25 changes: 25 additions & 0 deletions examples/systemd-socket-activation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
This allows Zot to indirectly listen at a privileged socket port (e.g. `443`) without granting it the `CAP_NET_BIND_SERVICE` capability.

This uses the [systemd Socket Activation](https://0pointer.de/blog/projects/socket-activated-containers.html) feature to create the listening socket at the privileged port. The port is defined by the `ListenStream` variable declared in the [`zot.socket` file](zot.socket).

At the first socket client connection, systemd will start the `zot` service, and will pass it the listening socket in the file descriptor defined by the `LISTEN_FDS` environment variable.

To install the `zot` service as described, review the example [`zot.service`](zot.service) and [`zot.socket`](zot.socket) files, and then execute the following commands as the `root` user:

```bash
install zot.service /etc/systemd/system/zot.service
install zot.socket /etc/systemd/system/zot.socket
systemctl daemon-reload
systemctl enable zot.service zot.socket
systemctl restart zot.service zot.socket
```

At development time, you can test the systemd Socket Activation using something like:

```bash
systemd-socket-activate \
--listen=127.0.0.1:9999 \
./bin/zot-linux-amd64 \
serve \
examples/config-minimal.json
```
16 changes: 16 additions & 0 deletions examples/systemd-socket-activation/zot.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[Unit]
Description=OCI Distribution Registry
Documentation=https://github.com/project-zot/zot
After=network.target auditd.service local-fs.target
Requires=zot.socket

[Service]
Type=simple
ExecStart=/usr/bin/zot serve /etc/zot/config.json
Restart=on-failure
User=zot
Group=zot
LimitNOFILE=500000

[Install]
WantedBy=multi-user.target
10 changes: 10 additions & 0 deletions examples/systemd-socket-activation/zot.socket
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=OCI Distribution Registry

[Socket]
ListenStream=80
FileDescriptorName=http
Service=zot.service

[Install]
WantedBy=sockets.target
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2
github.com/aws/aws-secretsmanager-caching-go v1.1.3
github.com/containers/image/v5 v5.29.1
github.com/coreos/go-systemd/v22 v22.5.0
github.com/google/go-github/v52 v52.0.0
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.2.2
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ github.com/containers/storage v1.51.0 h1:AowbcpiWXzAjHosKz7MKvPEqpyX+ryZA/ZurytR
github.com/containers/storage v1.51.0/go.mod h1:ybl8a3j1PPtpyaEi/5A6TOFs+5TrEyObeKJzVtkUlfc=
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
Expand Down
99 changes: 73 additions & 26 deletions pkg/api/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"syscall"
"time"

"github.com/coreos/go-systemd/v22/activation"
"github.com/gorilla/mux"
"github.com/zitadel/oidc/pkg/client/rp"

Expand Down Expand Up @@ -94,6 +95,73 @@ func (c *Controller) GetPort() int {
return c.chosenPort
}

func (c *Controller) createListener() (net.Listener, string, error) {

Check failure on line 98 in pkg/api/controller.go

View workflow job for this annotation

GitHub Actions / lint

block should not start with a whitespace (wsl)
// try to create the listener from the ambient systemd socket activation
// environment variables. otherwise, create the listener from the address
// defined in the configuration.

listeners, err := activation.Listeners()
if err != nil {
return nil, "", fmt.Errorf("systemd socket activation listeners failed to initialize: %w", err)
}

if len(listeners) == 1 {
listener := listeners[0]

c.Log.Info().Stringer("addr", listener.Addr()).Msg("using systemd socket activation")

_, port, err := net.SplitHostPort(listener.Addr().String())
if err != nil {
return nil, "", err
}

chosenPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return nil, "", err
}

c.chosenPort = int(chosenPort)

addr := fmt.Sprintf("%s:%d", c.Config.HTTP.Address, c.chosenPort)

return listener, addr, nil
}

if len(listeners) != 0 {
return nil, "", fmt.Errorf("systemd socket activation has an unexpected number of listeners: %w", err)
}

addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port)

listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, "", err
}

if c.Config.HTTP.Port == "0" || c.Config.HTTP.Port == "" {
chosenAddr, ok := listener.Addr().(*net.TCPAddr)
if !ok {
c.Log.Error().Str("port", c.Config.HTTP.Port).Msg("invalid addr type")

return nil, "", errors.ErrBadType
}
c.chosenPort = chosenAddr.Port

c.Log.Info().Int("port", chosenAddr.Port).IPAddr("address", chosenAddr.IP).Msg(
"port is unspecified, listening on kernel chosen port",
)
} else {
chosenPort, err := strconv.ParseUint(c.Config.HTTP.Port, 10, 16)
if err != nil {
return nil, "", err
}

c.chosenPort = int(chosenPort)
}

return listener, addr, nil
}

func (c *Controller) Run() error {
if err := c.initCookieStore(); err != nil {
return err
Expand Down Expand Up @@ -133,7 +201,11 @@ func (c *Controller) Run() error {
//nolint: contextcheck
_ = NewRouteHandler(c)

addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port)
listener, addr, err := c.createListener()
if err != nil {
return err
}

server := &http.Server{
Addr: addr,
Handler: c.Router,
Expand All @@ -142,31 +214,6 @@ func (c *Controller) Run() error {
}
c.Server = server

// Create the listener
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
}

if c.Config.HTTP.Port == "0" || c.Config.HTTP.Port == "" {
chosenAddr, ok := listener.Addr().(*net.TCPAddr)
if !ok {
c.Log.Error().Str("port", c.Config.HTTP.Port).Msg("invalid addr type")

return errors.ErrBadType
}

c.chosenPort = chosenAddr.Port

c.Log.Info().Int("port", chosenAddr.Port).IPAddr("address", chosenAddr.IP).Msg(
"port is unspecified, listening on kernel chosen port",
)
} else {
chosenPort, _ := strconv.ParseInt(c.Config.HTTP.Port, 10, 64)

c.chosenPort = int(chosenPort)
}

if c.Config.HTTP.TLS != nil && c.Config.HTTP.TLS.Key != "" && c.Config.HTTP.TLS.Cert != "" {
server.TLSConfig = &tls.Config{
CipherSuites: []uint16{
Expand Down

0 comments on commit 43ca3c7

Please sign in to comment.