Skip to content

Commit

Permalink
core: Add optional unix socket file permissions (#4741)
Browse files Browse the repository at this point in the history
* core: Add optional unix socket file permissions

This commit also changes the default unix socket file permissions to `u=w,g=,o=` (octal: `0200`).
It used to default to the shell's umask (usually `u=rwx,g=rx,o=rx`, octal: `0755`).

`/run/caddy.sock` -> `/run/caddy.sock` with `0200` default perms
`/run/caddy.sock|0222` -> `/run/caddy.sock` with `0222` perms

`|` instead of `:` is used as a separator, to account for the `:` in Windows drive letters (e.g. `C:\absolute\path.sock`)

Fun fact:
The old unix(7) man page (pre Jun 2016) stated a socket needs both read and write perms.
Turns out, only write perms are needed.
Corrected in mkerrisk/man-pages@7578ea2
Despite this, most implementations still default to read+write to this date.

* Add cases with Windows paths to test

* Require write perms for the owning user
  • Loading branch information
emilylange committed Jun 23, 2023
1 parent 7a69ae7 commit 22927e2
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 3 deletions.
64 changes: 61 additions & 3 deletions listeners.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"net"
"net/netip"
"os"
Expand Down Expand Up @@ -148,11 +149,27 @@ func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net
func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
var ln any
var err error
var address string
var unixFileMode fs.FileMode

address := na.JoinHostPort(portOffset)
// split unix socket addr early so lnKey
// is independent of permissions bits
if na.IsUnixNetwork() {
var err error
address, unixFileMode, err = splitUnixSocketPermissionsBits(na.Host)
if err != nil {
return nil, err
}
} else {
address = na.JoinHostPort(portOffset)
}

// if this is a unix socket, see if we already have it open
// if this is a unix socket, see if we already have it open,
// force socket permissions on it and return early
if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
if err := os.Chmod(address, unixFileMode); err != nil {
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
}
return socket, err
}

Expand Down Expand Up @@ -193,6 +210,12 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
unixSockets[lnKey] = unix
}

if IsUnixNetwork(na.Network) {
if err := os.Chmod(address, unixFileMode); err != nil {
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
}
}

return ln, nil
}

Expand Down Expand Up @@ -288,6 +311,40 @@ func IsUnixNetwork(netw string) bool {
return strings.HasPrefix(netw, "unix")
}

// Takes a unix socket address in the unusual "path|bits" format
// (e.g. /run/caddy.sock|0222) and tries to split it into
// socket path (host) and permissions bits (port). Colons (":")
// can't be used as separator, as socket paths on Windows may
// include a drive letter (e.g. `unix/c:\absolute\path.sock`).
// Permission bits will default to 0200 if none are specified.
// Throws an error, if the first carrying bit does not
// include write perms (e.g. `0422` or `022`).
// Symbolic permission representation (e.g. `u=w,g=w,o=w`)
// is not supported and will throw an error for now!
func splitUnixSocketPermissionsBits(addr string) (path string, fileMode fs.FileMode, err error) {
addrSplit := strings.SplitN(addr, "|", 2)

if len(addrSplit) == 2 {
// parse octal permission bit string as uint32
fileModeUInt64, err := strconv.ParseUint(addrSplit[1], 8, 32)
if err != nil {
return "", 0, fmt.Errorf("could not parse octal permission bits in %s: %v", addr, err)
}
fileMode = fs.FileMode(fileModeUInt64)

// FileMode.String() returns a string like `-rwxr-xr--` for `u=rwx,g=rx,o=r` (`0754`)
if string(fileMode.String()[2]) != "w" {
return "", 0, fmt.Errorf("owner of the socket requires '-w-' (write, octal: '2') permissions at least; got '%s' in %s", fileMode.String()[1:4], addr)
}

return addrSplit[0], fileMode, nil
}

// default to 0200 (symbolic: `u=w,g=,o=`)
// if no permission bits are specified
return addr, 0200, nil
}

// ParseNetworkAddress parses addr into its individual
// components. The input string is expected to be of
// the form "network/host:port-range" where any part is
Expand All @@ -312,10 +369,11 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
network = defaultNetwork
}
if IsUnixNetwork(network) {
_, _, err := splitUnixSocketPermissionsBits(host)
return NetworkAddress{
Network: network,
Host: host,
}, nil
}, err
}
var start, end uint64
if port == "" {
Expand Down
95 changes: 95 additions & 0 deletions listeners_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,3 +555,98 @@ func TestExpand(t *testing.T) {
}
}
}

func TestSplitUnixSocketPermissionsBits(t *testing.T) {
for i, tc := range []struct {
input string
expectNetwork string
expectPath string
expectFileMode string
expectErr bool
}{
{
input: "./foo.socket",
expectPath: "./foo.socket",
expectFileMode: "--w-------",
},
{
input: `.\relative\path.socket`,
expectPath: `.\relative\path.socket`,
expectFileMode: "--w-------",
},
{
// literal colon in resulting address
// and defaulting to 0200 bits
input: "./foo.socket:0666",
expectPath: "./foo.socket:0666",
expectFileMode: "--w-------",
},
{
input: "./foo.socket|0220",
expectPath: "./foo.socket",
expectFileMode: "--w--w----",
},
{
input: "/var/run/foo|222",
expectPath: "/var/run/foo",
expectFileMode: "--w--w--w-",
},
{
input: "./foo.socket|0660",
expectPath: "./foo.socket",
expectFileMode: "-rw-rw----",
},
{
input: "./foo.socket|0666",
expectPath: "./foo.socket",
expectFileMode: "-rw-rw-rw-",
},
{
input: "/var/run/foo|666",
expectPath: "/var/run/foo",
expectFileMode: "-rw-rw-rw-",
},
{
input: `c:\absolute\path.socket|220`,
expectPath: `c:\absolute\path.socket`,
expectFileMode: "--w--w----",
},
{
// symbolic permission representation is not supported for now
input: "./foo.socket|u=rw,g=rw,o=rw",
expectErr: true,
},
{
// octal (base-8) permission representation has to be between
// `0` for no read, no write, no exec (`---`) and
// `7` for read (4), write (2), exec (1) (`rwx` => `4+2+1 = 7`)
input: "./foo.socket|888",
expectErr: true,
},
{
// too many colons in address
input: "./foo.socket|123456|0660",
expectErr: true,
},
{
// owner is missing write perms
input: "./foo.socket|0522",
expectErr: true,
},
} {
actualPath, actualFileMode, err := splitUnixSocketPermissionsBits(tc.input)
if tc.expectErr && err == nil {
t.Errorf("Test %d: Expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("Test %d: Expected no error but got: %v", i, err)
}
if actualPath != tc.expectPath {
t.Errorf("Test %d: Expected path '%s' but got '%s'", i, tc.expectPath, actualPath)
}
// fileMode.Perm().String() parses 0 to "----------"
if !tc.expectErr && actualFileMode.Perm().String() != tc.expectFileMode {
t.Errorf("Test %d: Expected perms '%s' but got '%s'", i, tc.expectFileMode, actualFileMode.Perm().String())
}
}
}

2 comments on commit 22927e2

@lxhao61
Copy link

@lxhao61 lxhao61 commented on 22927e2 Jun 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This update affects listeners using abstract sockets, which prevents them from starting.
For example:

{
	"apps": {
		"http": {
			"servers": {
				"http2": {
					"listen": ["unix/@http2.sock"],
					"routes": [
						{
							"handle": [{
								"handler": "static_response",
								"body": "Hello, world!"
							}]
						}
					],
					"protocols": ["h1","h2"]
				}
			}
		}
	}
}

The error log is as follows:
Jun 24 14:25:39 ip-172-31-37-181 systemd[1]: journalctl -u caddy --no-pager
Jun 24 14:25:39 ip-172-31-37-181 systemd[1]: Starting Caddy...
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: {"level":"warn","ts":1687616739.6567402,"msg":"unable to determine directory for user configuration; falling back to current directory","error":"neither $XDG_CONFIG_HOME nor $HOME are defined"}
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: caddy.HomeDir=.
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: caddy.AppDataDir=./caddy
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: {"level":"warn","ts":1687616739.6830947,"msg":"unable to determine directory for user configuration; falling back to current directory","error":"neither $XDG_CONFIG_HOME nor $HOME are defined"}
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: caddy.AppConfigDir=./caddy
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: caddy.ConfigAutosavePath=caddy/autosave.json
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: caddy.Version=v2.7.0-beta.2.0.20230623204941-22927e278dc2 h1:zy15qANzKVQkmRPvQv7pu0HhwkVKYMU4jTOVmev3AIU=
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: runtime.GOOS=linux
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: runtime.GOARCH=amd64
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: runtime.Compiler=gc
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: runtime.NumCPU=1
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: runtime.GOMAXPROCS=1
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: runtime.Version=go1.20.5
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: os.Getwd=/
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: LANG=C.UTF-8
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: NOTIFY_SOCKET=/run/systemd/notify
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: LOGNAME=nobody
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: USER=nobody
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: INVOCATION_ID=686a96898e45437783ad13c3f77c4f01
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: JOURNAL_STREAM=8:211591
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: {"level":"info","ts":1687616739.6852288,"msg":"using provided configuration","config_file":"/usr/local/etc/caddy/caddy.json","config_adapter":""}
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: {"level":"info","ts":1687616739.6862564,"msg":"redirected default logger","from":"stderr","to":"/var/log/caddy/error.log"}
Jun 24 14:25:39 ip-172-31-37-181 caddy[2287]: Error: loading initial config: loading new config: http app module: start: listening on unix/@http2.sock: unable to set permissions (--w-------) on @http2.sock: chmod @http2.sock: no such file or directory
Jun 24 14:25:39 ip-172-31-37-181 systemd[1]: caddy.service: Main process exited, code=exited, status=1/FAILURE
Jun 24 14:25:39 ip-172-31-37-181 systemd[1]: caddy.service: Failed with result 'exit-code'.
Jun 24 14:25:39 ip-172-31-37-181 systemd[1]: Failed to start Caddy.
root@ip-172-31-37-181:~#

@emilylange
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lxhao61 could you please open an issue instead? :)

Will look into this soon (and fix it).
Thank you!

Please sign in to comment.