mirror of
https://tildegit.org/solderpunk/molly-brown
synced 2024-05-14 23:36:03 +02:00
Add support for chroot()ing server early after startup, more work toward issue #16.
This commit is contained in:
parent
06c6d190a6
commit
8372142843
16
README.md
16
README.md
|
@ -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
|
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
|
run by the superuser/root, then Molly will change its UID to that of
|
||||||
the `nobody` user before accepting network connections, so CGI
|
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
|
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
|
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
|
status code of 60. Requests made with a certificate not in the list
|
||||||
will cause a response with a status code of 60.
|
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
|
## .molly files
|
||||||
|
|
||||||
In order to allow users of shared-hosting who do not have access to
|
In order to allow users of shared-hosting who do not have access to
|
||||||
|
|
22
config.go
22
config.go
|
@ -16,6 +16,7 @@ type Config struct {
|
||||||
KeyPath string
|
KeyPath string
|
||||||
DocBase string
|
DocBase string
|
||||||
HomeDocBase string
|
HomeDocBase string
|
||||||
|
ChrootDir string
|
||||||
GeminiExt string
|
GeminiExt string
|
||||||
DefaultLang string
|
DefaultLang string
|
||||||
DefaultEncoding string
|
DefaultEncoding string
|
||||||
|
@ -59,6 +60,7 @@ func getConfig(filename string) (Config, error) {
|
||||||
config.KeyPath = "key.pem"
|
config.KeyPath = "key.pem"
|
||||||
config.DocBase = "/var/gemini/"
|
config.DocBase = "/var/gemini/"
|
||||||
config.HomeDocBase = "users"
|
config.HomeDocBase = "users"
|
||||||
|
config.ChrootDir = ""
|
||||||
config.GeminiExt = "gmi"
|
config.GeminiExt = "gmi"
|
||||||
config.DefaultLang = ""
|
config.DefaultLang = ""
|
||||||
config.DefaultEncoding = ""
|
config.DefaultEncoding = ""
|
||||||
|
@ -92,13 +94,29 @@ func getConfig(filename string) (Config, error) {
|
||||||
return config, errors.New("Invalid DirectorySort value.")
|
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) {
|
if !filepath.IsAbs(config.DocBase) {
|
||||||
abs, err := filepath.Abs(config.DocBase)
|
abs, err := filepath.Abs(config.DocBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config, err
|
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
|
// Absolutise CGI paths
|
||||||
|
|
|
@ -44,7 +44,9 @@ func handleGeminiRequest(conn net.Conn, config Config, accessLogEntries chan Log
|
||||||
log.RemoteAddr = conn.RemoteAddr()
|
log.RemoteAddr = conn.RemoteAddr()
|
||||||
log.RequestURL = "-"
|
log.RequestURL = "-"
|
||||||
log.Status = 0
|
log.Status = 0
|
||||||
defer func() { accessLogEntries <- log }()
|
if accessLogEntries != nil {
|
||||||
|
defer func() { accessLogEntries <- log }()
|
||||||
|
}
|
||||||
|
|
||||||
// Read request
|
// Read request
|
||||||
URL, err := readRequest(conn, &log, errorLog)
|
URL, err := readRequest(conn, &log, errorLog)
|
||||||
|
|
66
main.go
66
main.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -41,6 +42,29 @@ func main() {
|
||||||
log.Fatal(err)
|
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
|
// Open log files
|
||||||
var errorLogFile *os.File
|
var errorLogFile *os.File
|
||||||
if config.ErrorLog == "" {
|
if config.ErrorLog == "" {
|
||||||
|
@ -55,13 +79,9 @@ func main() {
|
||||||
errorLog := log.New(errorLogFile, "", log.Ldate|log.Ltime)
|
errorLog := log.New(errorLogFile, "", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
var accessLogFile *os.File
|
var accessLogFile *os.File
|
||||||
// TODO: Find a more elegant/portable way to disable logging
|
|
||||||
if config.AccessLog == "" {
|
|
||||||
config.AccessLog = "/dev/null"
|
|
||||||
}
|
|
||||||
if config.AccessLog == "-" {
|
if config.AccessLog == "-" {
|
||||||
accessLogFile = os.Stdout
|
accessLogFile = os.Stdout
|
||||||
} else {
|
} else if config.AccessLog != "" {
|
||||||
accessLogFile, err = os.OpenFile(config.AccessLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
accessLogFile, err = os.OpenFile(config.AccessLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorLog.Println("Error opening access log file: " + err.Error())
|
errorLog.Println("Error opening access log file: " + err.Error())
|
||||||
|
@ -92,11 +112,15 @@ func main() {
|
||||||
ClientAuth: tls.RequestClientCert,
|
ClientAuth: tls.RequestClientCert,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chdir to / so we don't block any mountpoints
|
// Try to chdir to /, so we don't block any mountpoints
|
||||||
err = os.Chdir("/")
|
// But if we can't for some reason it's no big deal
|
||||||
if err != nil {
|
err = os.Chdir("/")
|
||||||
errorLog.Println("Could not change working directory to /: " + err.Error())
|
if err != nil {
|
||||||
}
|
errorLog.Println("Could not change working directory to /: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply security restrictions
|
||||||
|
enableSecurityRestrictions(config, nobody_uid, errorLog)
|
||||||
|
|
||||||
// Create TLS listener
|
// Create TLS listener
|
||||||
listener, err := tls.Listen("tcp", ":"+strconv.Itoa(config.Port), tlscfg)
|
listener, err := tls.Listen("tcp", ":"+strconv.Itoa(config.Port), tlscfg)
|
||||||
|
@ -107,16 +131,18 @@ func main() {
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
// Start log handling routines
|
// Start log handling routines
|
||||||
accessLogEntries := make(chan LogEntry, 10)
|
var accessLogEntries chan LogEntry
|
||||||
go func() {
|
if config.AccessLog == "" {
|
||||||
for {
|
accessLogEntries = nil
|
||||||
entry := <-accessLogEntries
|
} else {
|
||||||
writeLogEntry(accessLogFile, entry)
|
accessLogEntries := make(chan LogEntry, 10)
|
||||||
}
|
go func() {
|
||||||
}()
|
for {
|
||||||
|
entry := <-accessLogEntries
|
||||||
// Restrict access to the files specified in config
|
writeLogEntry(accessLogFile, entry)
|
||||||
enableSecurityRestrictions(config, errorLog)
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// Start listening for signals
|
// Start listening for signals
|
||||||
shutdown := make(chan struct{})
|
shutdown := make(chan struct{})
|
||||||
|
|
|
@ -5,41 +5,23 @@ package main
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DropPrivs(config Config, errorLog *log.Logger) {
|
func DropPrivs(config Config, nobody_uid int, errorLog *log.Logger) {
|
||||||
|
|
||||||
// Get our real and effective UIDs
|
// Get our real and effective UIDs
|
||||||
uid := os.Getuid()
|
uid := os.Getuid()
|
||||||
euid := os.Geteuid()
|
euid := os.Geteuid()
|
||||||
|
|
||||||
// If these are equal and non-zero, there's nothing to do
|
// Are we root or are we running as a setuid binary?
|
||||||
if uid == euid && uid != 0 {
|
if uid == 0 || uid != euid {
|
||||||
return
|
err := syscall.Setuid(nobody_uid)
|
||||||
}
|
|
||||||
|
|
||||||
// If our real UID is root, we need to lookup the nobody UID
|
|
||||||
if uid == 0 {
|
|
||||||
user, err := user.Lookup("nobody")
|
|
||||||
if err != nil {
|
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)
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,10 @@ import (
|
||||||
// operations available to the molly brown executable. Please note that (S)CGI
|
// operations available to the molly brown executable. Please note that (S)CGI
|
||||||
// processes that molly brown spawns or communicates with are unrestricted
|
// processes that molly brown spawns or communicates with are unrestricted
|
||||||
// and should pledge their own restrictions and unveil their own files.
|
// 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
|
// Setuid to an unprivileged user
|
||||||
DropPrivs(config, errorLog)
|
DropPrivs(config, nobody_uid, errorLog)
|
||||||
|
|
||||||
// Unveil the configured document base as readable.
|
// Unveil the configured document base as readable.
|
||||||
log.Println("Unveiling \"" + config.DocBase + "\" as readable.")
|
log.Println("Unveiling \"" + config.DocBase + "\" as readable.")
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func enableSecurityRestrictions(config Config, errorLog *log.Logger) {
|
func enableSecurityRestrictions(config Config, nobody_uid int, errorLog *log.Logger) {
|
||||||
|
|
||||||
// Setuid to an unprivileged user
|
// Setuid to an unprivileged user
|
||||||
DropPrivs(config, errorLog)
|
DropPrivs(config, nobody_uid, errorLog)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue