1
1
Fork 0
mirror of https://tildegit.org/solderpunk/molly-brown synced 2024-04-28 19:05:03 +02:00

Add support for chroot()ing server early after startup, more work toward issue #16.

This commit is contained in:
Solderpunk 2023-02-15 21:10:22 +01:00
parent 06c6d190a6
commit 8372142843
7 changed files with 93 additions and 51 deletions

View File

@ -319,7 +319,12 @@ by the user who runs the binary. CGI processes will then be unable to
read any of those sensitive files. If the binary is not SETUID but is
run by the superuser/root, then Molly will change its UID to that of
the `nobody` user before accepting network connections, so CGI
processes will again not be able to read sensitive files.
processes will again not be able to read sensitive files. Note that
while these measures can protect Molly's own sensitive files from
CGI processes, CGI processes may still be able to read other sensitive
files anywhere else on the system. Consider chroot()-ing Molly Brown
into a small corner of the filesystem (see `ChrootDir` below) to
reduce this risk.
When compiled on GNU/Linux with Go versions 1.15 or earlier, Molly
Brown is completley unable to reliably change its UID due to the way
@ -382,6 +387,15 @@ facility.
status code of 60. Requests made with a certificate not in the list
will cause a response with a status code of 60.
### Security settings
* `ChrootDir`: A directory to which Molly Brown should chroot(),
making it more difficult for the server itself or spawned CGI
processes to read or write any files higher in the hiearch. The
chroot happens immediately after reading the config file. All other
paths specified in the config file (e.g. `DocBase`, `KeyPath`,
`AccessLog`) must be specified relative to `ChrootDir`.
## .molly files
In order to allow users of shared-hosting who do not have access to

View File

@ -16,6 +16,7 @@ type Config struct {
KeyPath string
DocBase string
HomeDocBase string
ChrootDir string
GeminiExt string
DefaultLang string
DefaultEncoding string
@ -59,6 +60,7 @@ func getConfig(filename string) (Config, error) {
config.KeyPath = "key.pem"
config.DocBase = "/var/gemini/"
config.HomeDocBase = "users"
config.ChrootDir = ""
config.GeminiExt = "gmi"
config.DefaultLang = ""
config.DefaultEncoding = ""
@ -92,13 +94,29 @@ func getConfig(filename string) (Config, error) {
return config, errors.New("Invalid DirectorySort value.")
}
// Absolutise DocBase
// Validate chroot() dir
if config.ChrootDir != "" {
config.ChrootDir = filepath.Abs(config.ChrootDir)
_, err := os.Stat(config.ChrootDir)
if os.IsNotExist(err) {
return config, err
}
}
// Absolutise DocBase, relative to the chroot dir
if !filepath.IsAbs(config.DocBase) {
abs, err := filepath.Abs(config.DocBase)
if err != nil {
return config, err
}
config.DocBase = abs
if config.ChrootDir != "" {
config.DocBase, err = filepath.Rel(config.ChrootDir, abs)
if err != nil {
return config, err
}
} else {
config.DocBase = abs
}
}
// Absolutise CGI paths

View File

@ -44,7 +44,9 @@ func handleGeminiRequest(conn net.Conn, config Config, accessLogEntries chan Log
log.RemoteAddr = conn.RemoteAddr()
log.RequestURL = "-"
log.Status = 0
defer func() { accessLogEntries <- log }()
if accessLogEntries != nil {
defer func() { accessLogEntries <- log }()
}
// Read request
URL, err := readRequest(conn, &log, errorLog)

66
main.go
View File

@ -7,6 +7,7 @@ import (
"log"
"os"
"os/signal"
"os/user"
"strconv"
"sync"
"syscall"
@ -41,6 +42,29 @@ func main() {
log.Fatal(err)
}
// If we are running as root, find the UID of the "nobody" user, before a
// chroot() possibly stops seeing /etc/passwd
uid := os.Getuid()
nobody_uid := -1
if uid == 0 {
nobody_user, err := user.Lookup("nobody")
if err != nil {
log.Fatal("Running as root but could not lookup UID for user " + "nobody" + ": " + err.Error())
}
nobody_uid, err = strconv.Atoi(nobody_user.Uid)
if err != nil {
log.Fatal("Running as root but could not lookup UID fr user " + "nobody" + ": " + err.Error())
}
}
// Chroot, if asked
if config.ChrootDir != "" {
err := syscall.Chroot(config.ChrootDir)
if err != nil {
log.Fatal("Could not chroot to " + config.ChrootDir + ": " + err.Error())
}
}
// Open log files
var errorLogFile *os.File
if config.ErrorLog == "" {
@ -55,13 +79,9 @@ func main() {
errorLog := log.New(errorLogFile, "", log.Ldate|log.Ltime)
var accessLogFile *os.File
// TODO: Find a more elegant/portable way to disable logging
if config.AccessLog == "" {
config.AccessLog = "/dev/null"
}
if config.AccessLog == "-" {
accessLogFile = os.Stdout
} else {
} else if config.AccessLog != "" {
accessLogFile, err = os.OpenFile(config.AccessLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
errorLog.Println("Error opening access log file: " + err.Error())
@ -92,11 +112,15 @@ func main() {
ClientAuth: tls.RequestClientCert,
}
// Chdir to / so we don't block any mountpoints
err = os.Chdir("/")
if err != nil {
errorLog.Println("Could not change working directory to /: " + err.Error())
}
// Try to chdir to /, so we don't block any mountpoints
// But if we can't for some reason it's no big deal
err = os.Chdir("/")
if err != nil {
errorLog.Println("Could not change working directory to /: " + err.Error())
}
// Apply security restrictions
enableSecurityRestrictions(config, nobody_uid, errorLog)
// Create TLS listener
listener, err := tls.Listen("tcp", ":"+strconv.Itoa(config.Port), tlscfg)
@ -107,16 +131,18 @@ func main() {
defer listener.Close()
// Start log handling routines
accessLogEntries := make(chan LogEntry, 10)
go func() {
for {
entry := <-accessLogEntries
writeLogEntry(accessLogFile, entry)
}
}()
// Restrict access to the files specified in config
enableSecurityRestrictions(config, errorLog)
var accessLogEntries chan LogEntry
if config.AccessLog == "" {
accessLogEntries = nil
} else {
accessLogEntries := make(chan LogEntry, 10)
go func() {
for {
entry := <-accessLogEntries
writeLogEntry(accessLogFile, entry)
}
}()
}
// Start listening for signals
shutdown := make(chan struct{})

View File

@ -5,41 +5,23 @@ package main
import (
"log"
"os"
"os/user"
"strconv"
"syscall"
)
func DropPrivs(config Config, errorLog *log.Logger) {
func DropPrivs(config Config, nobody_uid int, errorLog *log.Logger) {
// Get our real and effective UIDs
uid := os.Getuid()
euid := os.Geteuid()
// If these are equal and non-zero, there's nothing to do
if uid == euid && uid != 0 {
return
}
// If our real UID is root, we need to lookup the nobody UID
if uid == 0 {
user, err := user.Lookup("nobody")
// Are we root or are we running as a setuid binary?
if uid == 0 || uid != euid {
err := syscall.Setuid(nobody_uid)
if err != nil {
errorLog.Println("Could not lookup UID for user " + "nobody" + ": " + err.Error())
errorLog.Println("Could not setuid to " + strconv.Itoa(uid) + ": " + err.Error())
log.Fatal(err)
}
uid, err = strconv.Atoi(user.Uid)
if err != nil {
errorLog.Println("Could not lookup UID fr user " + "nobody" + ": " + err.Error())
log.Fatal(err)
}
}
// Drop priveleges
err := syscall.Setuid(uid)
if err != nil {
errorLog.Println("Could not setuid to " + strconv.Itoa(uid) + ": " + err.Error())
log.Fatal(err)
}
}

View File

@ -11,10 +11,10 @@ import (
// operations available to the molly brown executable. Please note that (S)CGI
// processes that molly brown spawns or communicates with are unrestricted
// and should pledge their own restrictions and unveil their own files.
func enableSecurityRestrictions(config Config, errorLog *log.Logger) {
func enableSecurityRestrictions(config Config, nobody_uid int, errorLog *log.Logger) {
// Setuid to an unprivileged user
DropPrivs(config, errorLog)
DropPrivs(config, nobody_uid, errorLog)
// Unveil the configured document base as readable.
log.Println("Unveiling \"" + config.DocBase + "\" as readable.")

View File

@ -6,8 +6,8 @@ import (
"log"
)
func enableSecurityRestrictions(config Config, errorLog *log.Logger) {
func enableSecurityRestrictions(config Config, nobody_uid int, errorLog *log.Logger) {
// Setuid to an unprivileged user
DropPrivs(config, errorLog)
DropPrivs(config, nobody_uid, errorLog)
}