WIP: Add Privilege droppings
This is a work in progress commit. Do not merge! Adds privilege droppings, such that the main webserver process must not run as root anymore.
This commit is contained in:
parent
1bdcbd7904
commit
7721f4fcb9
168
cmd/weblug/child.go
Normal file
168
cmd/weblug/child.go
Normal file
|
@ -0,0 +1,168 @@
|
|||
package main
|
||||
|
||||
/* Child process handling */
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type Process struct {
|
||||
pid int // Child PID or 0, if not running
|
||||
cmd string // Command
|
||||
argv []string // Arguments
|
||||
pipeIn int // stdin pipe file descriptor
|
||||
pipeOut int // stdout pipe file descriptor
|
||||
pipeErr int // stderr pipe file descriptor
|
||||
env []string
|
||||
}
|
||||
|
||||
type ProcessPool struct {
|
||||
Processes []Process
|
||||
}
|
||||
|
||||
// Create a Unix pipe
|
||||
func pipe() (int, int, error) {
|
||||
fd := make([]int, 2)
|
||||
err := syscall.Pipe(fd)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return fd[0], fd[1], nil
|
||||
}
|
||||
|
||||
func CreateProcess(cmd string, argv []string) Process {
|
||||
child := Process{cmd: cmd, argv: argv}
|
||||
return child
|
||||
}
|
||||
|
||||
func (c *Process) SetEnv(env []string) {
|
||||
c.env = env
|
||||
}
|
||||
|
||||
func (c *Process) Close() {
|
||||
// Close all pipes
|
||||
if c.pipeIn != 0 {
|
||||
syscall.Close(c.pipeIn)
|
||||
c.pipeIn = 0
|
||||
}
|
||||
if c.pipeOut != 0 {
|
||||
syscall.Close(c.pipeOut)
|
||||
c.pipeOut = 0
|
||||
}
|
||||
if c.pipeErr != 0 {
|
||||
syscall.Close(c.pipeErr)
|
||||
c.pipeErr = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Process) Exec() error {
|
||||
var err error
|
||||
var (
|
||||
pInIn, pOutOut, pOutErr int
|
||||
)
|
||||
|
||||
// Create in- and output pipe
|
||||
pInIn, c.pipeIn, err = pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.pipeOut, pOutOut, err = pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.pipeErr, pOutErr, err = pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attr := syscall.ProcAttr{
|
||||
// Use pipes as stdin, stdout and stderr
|
||||
Files: []uintptr{uintptr(pInIn), uintptr(pOutOut), uintptr(pOutErr)},
|
||||
Env: c.env,
|
||||
}
|
||||
c.pid, err = syscall.ForkExec(c.cmd, c.argv, &attr)
|
||||
// Close pipe ends which go to the process
|
||||
syscall.Close(pInIn)
|
||||
syscall.Close(pOutOut)
|
||||
syscall.Close(pOutErr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait waits for the process to complete, returning the return value
|
||||
func (c *Process) Wait() (int, error) {
|
||||
if c.pid == 0 {
|
||||
return 0, fmt.Errorf("no process")
|
||||
}
|
||||
proc, err := os.FindProcess(c.pid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
state, err := proc.Wait()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
c.pid = 0
|
||||
return state.ExitCode(), err
|
||||
}
|
||||
|
||||
// Write writes a given buffer to the stdin of the child process
|
||||
func (c *Process) Write(p []byte) (int, error) {
|
||||
if c.pid == 0 {
|
||||
return 0, fmt.Errorf("no process")
|
||||
}
|
||||
if c.pipeIn == 0 {
|
||||
return 0, fmt.Errorf("no pipe")
|
||||
}
|
||||
return syscall.Write(c.pipeIn, p)
|
||||
}
|
||||
|
||||
// ReadStdout reads from the stdout of the child process
|
||||
func (c *Process) ReadStdout(p []byte) (int, error) {
|
||||
if c.pid == 0 {
|
||||
return 0, fmt.Errorf("no process")
|
||||
}
|
||||
if c.pipeOut == 0 {
|
||||
return 0, fmt.Errorf("no pipe")
|
||||
}
|
||||
return syscall.Read(c.pipeOut, p)
|
||||
}
|
||||
|
||||
// ReadStderr reads from the stderr of the child process
|
||||
func (c *Process) ReadStderr(p []byte) (int, error) {
|
||||
if c.pid == 0 {
|
||||
return 0, fmt.Errorf("no process")
|
||||
}
|
||||
if c.pipeErr == 0 {
|
||||
return 0, fmt.Errorf("no pipe")
|
||||
}
|
||||
return syscall.Read(c.pipeErr, p)
|
||||
}
|
||||
|
||||
func (c *Process) Terminate() {
|
||||
if c.pid != 0 {
|
||||
c.Close()
|
||||
syscall.Kill(c.pid, syscall.SIGTERM)
|
||||
c.pid = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Process) Abort() {
|
||||
if c.pid != 0 {
|
||||
c.Close()
|
||||
syscall.Kill(c.pid, syscall.SIGABRT)
|
||||
c.pid = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Process) Kill() {
|
||||
if c.pid != 0 {
|
||||
c.Close()
|
||||
syscall.Kill(c.pid, syscall.SIGKILL)
|
||||
c.pid = 0
|
||||
}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
@ -23,6 +26,8 @@ type Hook struct {
|
|||
BlockedAddresses []string `yaml:"blocked"` // Addresses that are explicitly blocked
|
||||
HttpBasicAuth BasicAuth `yaml:"basic_auth"` // Optional requires http basic auth
|
||||
Env map[string]string `yaml:"env"` // Optional environment variables
|
||||
|
||||
proc Process // Child process responsible for this webhook
|
||||
}
|
||||
|
||||
type BasicAuth struct {
|
||||
|
@ -78,33 +83,6 @@ func cmdSplit(command string) []string {
|
|||
return ret
|
||||
}
|
||||
|
||||
// Run executes the given command and return it's return code. It also respects the given concurrency number and will block until resources are free
|
||||
func (hook *Hook) Run() error {
|
||||
split := cmdSplit(hook.Command)
|
||||
args := make([]string, 0)
|
||||
if len(split) > 1 {
|
||||
args = split[1:]
|
||||
}
|
||||
cmd := exec.Command(split[0], args...)
|
||||
if hook.UID > 0 || hook.GID > 0 {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(hook.UID), Gid: uint32(hook.GID)}
|
||||
}
|
||||
cmd.Env = make([]string, 0)
|
||||
if hook.Env != nil {
|
||||
// Build environment variable list as expected by cmd.Env
|
||||
for k, v := range hook.Env {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
if hook.Output {
|
||||
buf, ret := cmd.Output()
|
||||
fmt.Println(string(buf))
|
||||
return ret
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func isAddressInList(addr string, addrList []string) (bool, error) {
|
||||
ip, _, err := net.ParseCIDR(addr)
|
||||
if err != nil {
|
||||
|
@ -180,3 +158,77 @@ func (hook *Hook) IsAddressAllowed(addr string) (bool, error) {
|
|||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Run as child process
|
||||
func hookChildProcessRun() {
|
||||
var err error
|
||||
|
||||
registerTerminationSignal()
|
||||
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Fprintf(os.Stderr, "not enough arguments\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := os.Args[2]
|
||||
uid_s := os.Args[3]
|
||||
gid_s := os.Args[3]
|
||||
uid, gid := 0, 0
|
||||
|
||||
// Drop privileges
|
||||
if uid_s != "" {
|
||||
uid, err = strconv.Atoi(uid_s)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid uid\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if gid_s != "" {
|
||||
gid, err = strconv.Atoi(gid_s)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid gid\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if gid > 0 {
|
||||
if err := syscall.Setgid(gid); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if uid > 0 {
|
||||
if err := syscall.Setuid(uid); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare command
|
||||
split := cmdSplit(cmd)
|
||||
args := make([]string, 0)
|
||||
if len(split) > 1 {
|
||||
args = split[1:]
|
||||
}
|
||||
|
||||
// Ready to receive the signal to run a webhook
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() { // Every newline is a signal, ignore the rest
|
||||
// Note: Env must have been sanatized before forking child process.
|
||||
cmd := exec.Command(split[0], args...)
|
||||
err := cmd.Run()
|
||||
buf, _ := cmd.Output()
|
||||
rc := 0
|
||||
if err != nil {
|
||||
rc = cmd.ProcessState.ExitCode()
|
||||
}
|
||||
// Failed
|
||||
fmt.Printf("%d %d\n", rc, len(buf))
|
||||
fmt.Println(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the given command and return it's return code. It also respects the given concurrency number and will block until resources are free
|
||||
func (hook *Hook) Run() error {
|
||||
// Trigger webhook child program
|
||||
if _, err := hook.proc.Write([]byte("\n")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,10 +25,10 @@ func usage() {
|
|||
fmt.Println("The program loads the given yaml files for webhook definitions")
|
||||
}
|
||||
|
||||
// awaits SIGINT or SIGTERM
|
||||
func awaitTerminationSignal() {
|
||||
// exit on SIGINT, SIGTERM or SIGABRT
|
||||
func registerTerminationSignal() {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
|
||||
go func() {
|
||||
sig := <-sigs
|
||||
fmt.Println(sig)
|
||||
|
@ -56,6 +56,11 @@ func main() {
|
|||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
}
|
||||
if os.Args[1] == "--hook" {
|
||||
// Webhook child process handler
|
||||
hookChildProcessRun()
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, arg := range os.Args[1:] {
|
||||
if arg == "" {
|
||||
continue
|
||||
|
@ -101,7 +106,7 @@ func main() {
|
|||
http.HandleFunc(hook.Route, createHandler(hook))
|
||||
}
|
||||
|
||||
awaitTerminationSignal()
|
||||
registerTerminationSignal()
|
||||
|
||||
log.Printf("Launching webserver on %s", cf.Settings.BindAddress)
|
||||
log.Fatal(http.ListenAndServe(cf.Settings.BindAddress, nil))
|
||||
|
@ -109,6 +114,19 @@ func main() {
|
|||
|
||||
// create a http handler function from the given hook
|
||||
func createHandler(hook Hook) Handler {
|
||||
// Create child process for this webhook
|
||||
prog := os.Args[0]
|
||||
hook.proc = CreateProcess(prog, []string{prog, "--hook", hook.Command, fmt.Sprintf("%d", hook.UID), fmt.Sprintf("%d", hook.GID)})
|
||||
// Build environment variable list as expected by cmd.Env
|
||||
hook.proc.env = make([]string, 0)
|
||||
for k, v := range hook.Env {
|
||||
hook.proc.env = append(hook.proc.env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
if err := hook.proc.Exec(); err != nil {
|
||||
// We cannot recover from this error
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("GET %s %s", r.RemoteAddr, hook.Name)
|
||||
|
||||
|
|
Loading…
Reference in a new issue