diff --git a/examples/systemd-socket-activation/README.md b/examples/systemd-socket-activation/README.md new file mode 100644 index 000000000..23397e8cc --- /dev/null +++ b/examples/systemd-socket-activation/README.md @@ -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 +``` diff --git a/examples/systemd-socket-activation/zot.service b/examples/systemd-socket-activation/zot.service new file mode 100644 index 000000000..0ae9afa35 --- /dev/null +++ b/examples/systemd-socket-activation/zot.service @@ -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 diff --git a/examples/systemd-socket-activation/zot.socket b/examples/systemd-socket-activation/zot.socket new file mode 100644 index 000000000..5d5ab5328 --- /dev/null +++ b/examples/systemd-socket-activation/zot.socket @@ -0,0 +1,10 @@ +[Unit] +Description=OCI Distribution Registry + +[Socket] +ListenStream=80 +FileDescriptorName=http +Service=zot.service + +[Install] +WantedBy=sockets.target diff --git a/go.mod b/go.mod index d1c8eefa0..b5e423fb2 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 344c6dd8b..169f23a9e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/api/controller.go b/pkg/api/controller.go index e8c167c38..ad01f65e3 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -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" @@ -94,6 +95,73 @@ func (c *Controller) GetPort() int { return c.chosenPort } +func (c *Controller) createListener() (net.Listener, string, error) { + // 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 @@ -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, @@ -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{