Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for systemd socket activation #2186

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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 @@
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)
Fixed Show fixed Hide fixed

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)
Fixed Show fixed Hide fixed
}

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 @@
//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 @@
}
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
Loading