Skip to content

Commit

Permalink
cmd/consrv: add experimental privilege dropping on gokrazy
Browse files Browse the repository at this point in the history
Implementation is more involved than NTP's simpler re-execute self and
reuse file descriptor 3 technique. We need privileges to read config
files, open serial port devices, and depending on the port choice of the
user (if <1024) listen on sockets.

Privileged Resource Acquisition Changes:
- Move SSH and debug HTTP server listening socket creation outside of
  goroutines to ensure all the privileged operations are sequentially
  ordered before privdrop.

- Modify `serveDebug` to receive a net.Listener and call
  `net.http.Server.Serve` instead.

Command Line Flag:
- Adds `-experimental-drop-privileges` boolean flag, defaulting to
  false.

Privdrop, when enabled:
- On Gokrazy:
  - Gated behind build constraint tag `gokrazy`
  - Creates an empty directory under /dev/shm and chroot()s to it.
  - Changes GID and UID to conventional nobody/nogroup ID 65534

- Other platforms:
  - Returns a "not implemented" error.

Co-authored-by: Matt Layher <mdlayher@gmail.com>
Signed-off-by: Matt Layher <mdlayher@gmail.com>
  • Loading branch information
bdd and mdlayher committed Jan 17, 2024
1 parent 62e3068 commit 609fdda
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 15 deletions.
61 changes: 46 additions & 15 deletions cmd/consrv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ import (

func main() {
var (
c = flag.String("c", "consrv.toml", "path to consrv.toml configuration file")
k = flag.String("k", "host_key", "path to OpenSSH format host key file")
c = flag.String("c", "consrv.toml", "path to consrv.toml configuration file")
k = flag.String("k", "host_key", "path to OpenSSH format host key file")
mustPrivdrop = flag.Bool("experimental-drop-privileges", false, "[EXPERIMENTAL] run as an unprivileged process and chroot to an empty dir")
)

flag.Parse()
Expand Down Expand Up @@ -160,33 +161,57 @@ func main() {
}
}

// Start the SSH server.
srv, err := newSSHServer(hostKey, devices, newIdentities(cfg, ll), ll, mm)
ids := newIdentities(cfg, ll)

// Start the SSH server and optional HTTP debug server.
sshl, err := net.Listen("tcp", cfg.Server.Address)
if err != nil {
ll.Fatalf("failed to create SSH server: %v", err)
ll.Fatalf("failed to listen for SSH server: %v", err)
}

var httpl net.Listener
if cfg.Debug.Address != "" {
l, err := net.Listen("tcp", cfg.Debug.Address)
if err != nil {
ll.Fatalf("failed to listen for HTTP debug server: %v", err)
}
httpl = l
}

if *mustPrivdrop {
// Experimental: drop privileges now that we're done reading
// configuration and opening possibly privileged TCP listeners.
info, err := dropPrivileges()
if err != nil {
ll.Fatalf("failed to drop privileges: %v", err)
}

ll.Printf("dropped privileges: chroot: %q, UID: %d GID: %d", info.Chroot, info.UID, info.GID)
}

var eg errgroup.Group

eg.Go(func() error {
l, err := net.Listen("tcp", cfg.Server.Address)
defer sshl.Close()

srv, err := newSSHServer(hostKey, devices, ids, ll, mm)
if err != nil {
return fmt.Errorf("failed to listen for SSH: %v", err)
return fmt.Errorf("failed to create SSH server: %w", err)
}
defer l.Close()

ll.Printf("starting SSH server on %q", cfg.Server.Address)
if err := srv.Serve(l); err != nil {
ll.Printf("starting SSH server on %q", sshl.Addr())
if err := srv.Serve(sshl); err != nil {
return fmt.Errorf("failed to serve SSH: %v", err)
}

return nil
})

// Enable debug server if an address is set.
if cfg.Debug.Address != "" {
if httpl != nil {
eg.Go(func() error {
if err := serveDebug(cfg.Debug, reg, ll); err != nil {
defer httpl.Close()

if err := serveDebug(cfg.Debug, reg, httpl, ll); err != nil {
return fmt.Errorf("failed to serve debug HTTP: %v", err)
}

Expand All @@ -199,8 +224,14 @@ func main() {
}
}

// privilegesInfo contains information from dropping privileges.
type privilegesInfo struct {
Chroot string
UID, GID int
}

// serveDebug starts the HTTP debug server with the input configuration.
func serveDebug(d debug, reg *prometheus.Registry, ll *log.Logger) error {
func serveDebug(d debug, reg *prometheus.Registry, listener net.Listener, ll *log.Logger) error {
mux := http.NewServeMux()

if d.Prometheus {
Expand All @@ -224,5 +255,5 @@ func serveDebug(d debug, reg *prometheus.Registry, ll *log.Logger) error {
Handler: mux,
}

return s.ListenAndServe()
return s.Serve(listener)
}
52 changes: 52 additions & 0 deletions cmd/consrv/privdrop_gokrazy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2023 Berk D. Demir and Matt Layher
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build gokrazy

package main

import (
"fmt"
"os"
"syscall"
)

const (
privdropUID = 65534 // conventionally: nobody
privdropGID = 65534 // conventionally: nogroup
)

func dropPrivileges() (*privilegesInfo, error) {
dir, err := os.MkdirTemp("/dev/shm", "consrv-chroot-*")
if err != nil {
return nil, fmt.Errorf("create chroot directory: %w", err)
}

if err := syscall.Chroot(dir); err != nil {
return nil, fmt.Errorf("chroot %q: %w", dir, err)
}

if err := syscall.Setgid(privdropGID); err != nil {
return nil, fmt.Errorf("setgid: %w", err)
}

if err := syscall.Setuid(privdropUID); err != nil {
return nil, fmt.Errorf("setuid: %w", err)
}

return &privilegesInfo{
Chroot: dir,
UID: privdropUID,
GID: privdropGID,
}, nil
}
25 changes: 25 additions & 0 deletions cmd/consrv/privdrop_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2023 Berk D. Demir and Matt Layher
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !gokrazy

package main

import (
"fmt"
"runtime"
)

func dropPrivileges() (*privilegesInfo, error) {
return nil, fmt.Errorf("implemented only on gokrazy, not on %s/%s", runtime.GOOS, runtime.GOARCH)
}

0 comments on commit 609fdda

Please sign in to comment.