From 548bef8188e7f667fa9cf21ce6840cc71b7b4e7c Mon Sep 17 00:00:00 2001 From: Michael du Breuil Date: Thu, 24 Oct 2024 21:11:16 -0600 Subject: [PATCH] Initial implementation of a sandbox for OpenBSD Leverages pledge and unveil, and leaves a public API for other systems to follow. The API was designed to match the OpenBSD side as that's the initial target, if a BPF/capsicum implementation is brought forward it may be worth changing the API, and we should be okay with that. --- cmd/gonic/gonic.go | 19 +++++++++++- db/db.go | 6 ++++ db/migrations.go | 5 ++- sandbox/none.go | 19 ++++++++++++ sandbox/sandbox_openbsd.go | 63 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 sandbox/none.go create mode 100644 sandbox/sandbox_openbsd.go diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go index 0d0e5099..fe967764 100644 --- a/cmd/gonic/gonic.go +++ b/cmd/gonic/gonic.go @@ -41,6 +41,7 @@ import ( "go.senan.xyz/gonic/listenbrainz" "go.senan.xyz/gonic/playlist" "go.senan.xyz/gonic/podcast" + "go.senan.xyz/gonic/sandbox" "go.senan.xyz/gonic/scanner" "go.senan.xyz/gonic/scrobble" "go.senan.xyz/gonic/server/ctrladmin" @@ -51,6 +52,7 @@ import ( ) func main() { + sandbox.Init() confListenAddr := flag.String("listen-addr", "0.0.0.0:4747", "listen address (optional)") confTLSCert := flag.String("tls-cert", "", "path to TLS certificate (optional)") @@ -98,7 +100,11 @@ func main() { flag.Parse() flagconf.ParseEnv() - flagconf.ParseConfig(*confConfigPath) + + if *confConfigPath != "" { + sandbox.ReadOnlyPath(*confConfigPath) + flagconf.ParseConfig(*confConfigPath) + } if *confShowVersion { fmt.Printf("v%s\n", gonic.Version) @@ -115,17 +121,21 @@ func main() { var err error for i, confMusicPath := range confMusicPaths { + sandbox.ReadOnlyPath(confMusicPath.path) if confMusicPaths[i].path, err = validatePath(confMusicPath.path); err != nil { log.Fatalf("checking music dir %q: %v", confMusicPath.path, err) } } + sandbox.ReadWriteCreatePath(*confPodcastPath) if *confPodcastPath, err = validatePath(*confPodcastPath); err != nil { log.Fatalf("checking podcast directory: %v", err) } + sandbox.ReadWriteCreatePath(*confCachePath) if *confCachePath, err = validatePath(*confCachePath); err != nil { log.Fatalf("checking cache directory: %v", err) } + sandbox.ReadWriteCreatePath(*confPlaylistsPath) if *confPlaylistsPath, err = validatePath(*confPlaylistsPath); err != nil { log.Fatalf("checking playlist directory: %v", err) } @@ -156,6 +166,13 @@ func main() { log.Panicf("error migrating database: %v\n", err) } + if *confTLSCert != "" && *confTLSKey != "" { + sandbox.ReadOnlyPath(*confTLSCert) + sandbox.ReadOnlyPath(*confTLSKey) + } + + sandbox.AllPathsAdded() + var musicPaths []ctrlsubsonic.MusicPath for _, pa := range confMusicPaths { musicPaths = append(musicPaths, ctrlsubsonic.MusicPath{Alias: pa.alias, Path: pa.path}) diff --git a/db/db.go b/db/db.go index ab5a9485..44598bb7 100644 --- a/db/db.go +++ b/db/db.go @@ -15,6 +15,8 @@ import ( "github.com/jinzhu/gorm" "github.com/mattn/go-sqlite3" + "go.senan.xyz/gonic/sandbox" + // TODO: remove this dep "go.senan.xyz/gonic/server/ctrlsubsonic/specid" ) @@ -48,6 +50,10 @@ func New(path string, options url.Values) (*DB, error) { Scheme: "file", Opaque: path, } + sandbox.ReadWriteCreatePath(path) + sandbox.ReadWriteCreatePath(path + "-wal") + sandbox.ReadWriteCreatePath(path + "-shm") + sandbox.ReadWriteCreatePath(path + "-journal") url.RawQuery = options.Encode() db, err := gorm.Open("sqlite3", url.String()) if err != nil { diff --git a/db/migrations.go b/db/migrations.go index 447bfdf6..a4ba8a6e 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -14,6 +14,7 @@ import ( "github.com/jinzhu/gorm" "go.senan.xyz/gonic/fileutil" "go.senan.xyz/gonic/playlist" + "go.senan.xyz/gonic/sandbox" "go.senan.xyz/gonic/server/ctrlsubsonic/specid" "gopkg.in/gormigrate.v1" ) @@ -734,7 +735,9 @@ func backupDBPre016(tx *gorm.DB, ctx MigrationContext) error { if !ctx.Production { return nil } - return Dump(context.Background(), tx, fmt.Sprintf("%s.%d.bak", ctx.DBPath, time.Now().Unix())) + backupPath := fmt.Sprintf("%s.%d.bak", ctx.DBPath, time.Now().Unix()) + sandbox.ReadWriteCreatePath(backupPath) + return Dump(context.Background(), tx, backupPath) } func migrateAlbumTagArtistString(tx *gorm.DB, _ MigrationContext) error { diff --git a/sandbox/none.go b/sandbox/none.go new file mode 100644 index 00000000..b10a9246 --- /dev/null +++ b/sandbox/none.go @@ -0,0 +1,19 @@ +//go:build !openbsd +// +build !openbsd + +package sandbox + +func Init() { +} + +func ReadOnlyPath(path string) { +} + +func ReadWritePath(path string) { +} + +func ReadWriteCreatePath(path string) { +} + +func AllPathsAdded() { +} diff --git a/sandbox/sandbox_openbsd.go b/sandbox/sandbox_openbsd.go new file mode 100644 index 00000000..f83dedff --- /dev/null +++ b/sandbox/sandbox_openbsd.go @@ -0,0 +1,63 @@ +package sandbox + +import ( + "log" + "os/exec" + + "golang.org/x/sys/unix" +) + +func Init() { + if err := unix.PledgePromises("stdio rpath cpath wpath flock inet unveil dns proc exec fattr"); err != nil { + log.Fatalf("failed to pledge: %v", err) + } + // find the transcoding and jukebox paths before doing any other unveils + // otherwise looking for it will fail + ffmpegPath, ffmpegErr := exec.LookPath("ffmpeg") + mpvPath, mpvErr := exec.LookPath("mpv") + if ffmpegErr == nil || mpvErr == nil { + if ffmpegErr == nil { + ExecPath(ffmpegPath) + } + if mpvErr == nil { + ExecPath(mpvPath) + } + } else { + // we can restrict our permissions + if err := unix.PledgePromises("stdio rpath cpath wpath flock inet unveil dns"); err != nil { + log.Fatalf("failed to pledge: %v", err) + } + } + // needed to enable certificate validation + ReadOnlyPath("/etc/ssl/cert.pem") +} + +func ExecPath(path string) { + if err := unix.Unveil(path, "rx"); err != nil { + log.Fatalf("failed to unveil exec for %s: %v", path, err) + } +} + +func ReadOnlyPath(path string) { + if err := unix.Unveil(path, "r"); err != nil { + log.Fatalf("failed to unveil read for %s: %v", path, err) + } +} + +func ReadWritePath(path string) { + if err := unix.Unveil(path, "rw"); err != nil { + log.Fatalf("failed to unveil read/write for %s: %v", path, err) + } +} + +func ReadWriteCreatePath(path string) { + if err := unix.Unveil(path, "rwc"); err != nil { + log.Fatalf("failed to unveil read/write/create for %s: %v", path, err) + } +} + +func AllPathsAdded() { + if err := unix.UnveilBlock(); err != nil { + log.Fatalf("failed to finalize unveil: %v", err) + } +}