aboutsummaryrefslogtreecommitdiffhomepage
path: root/hst/config.go
blob: 5032471b90e1c3b672d221d96475967f10cf34bc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package hst

import (
	"errors"
	"strconv"
	"strings"

	"hakurei.app/container/check"
)

// Config configures an application container, implemented in internal/app.
type Config struct {
	// Reverse-DNS style configured arbitrary identifier string.
	// Passed to wayland security-context-v1 and used as part of defaults in dbus session proxy.
	ID string `json:"id,omitempty"`

	// System services to make available in the container.
	Enablements *Enablements `json:"enablements,omitempty"`

	// Session D-Bus proxy configuration.
	// If set to nil, session bus proxy assume built-in defaults.
	SessionBus *BusConfig `json:"session_bus,omitempty"`
	// System D-Bus proxy configuration.
	// If set to nil, system bus proxy is disabled.
	SystemBus *BusConfig `json:"system_bus,omitempty"`

	// Direct access to wayland socket, no attempt is made to attach security-context-v1
	// and the bare socket is made available to the container.
	//
	// This option is unsupported and most likely enables full control over the Wayland
	// session. Do not set this to true unless you are sure you know what you are doing.
	DirectWayland bool `json:"direct_wayland,omitempty"`
	// Direct access to PulseAudio socket, no attempt is made to establish pipewire-pulse
	// server via a PipeWire socket with a SecurityContext attached and the bare socket
	// is made available to the container.
	//
	// This option is unsupported and enables arbitrary code execution as the PulseAudio
	// server. Do not set this to true, this is insecure under any configuration.
	DirectPulse bool `json:"direct_pulse,omitempty"`

	// Extra acl updates to perform before setuid.
	ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"`

	// Numerical application id, passed to hsu, used to derive init user namespace credentials.
	Identity int `json:"identity"`
	// Init user namespace supplementary groups inherited by all container processes.
	Groups []string `json:"groups"`

	// High level configuration applied to the underlying [container].
	Container *ContainerConfig `json:"container"`
}

var (
	// ErrConfigNull is returned by [Config.Validate] for an invalid configuration that contains a null value for any
	// field that must not be null.
	ErrConfigNull = errors.New("unexpected null in config")

	// ErrIdentityBounds is returned by [Config.Validate] for an out of bounds [Config.Identity] value.
	ErrIdentityBounds = errors.New("identity out of bounds")

	// ErrEnviron is returned by [Config.Validate] if an environment variable name contains '=' or NUL.
	ErrEnviron = errors.New("invalid environment variable name")

	// ErrInsecure is returned by [Config.Validate] if the configuration is considered insecure.
	ErrInsecure = errors.New("configuration is insecure")
)

// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
func (config *Config) Validate() error {
	if config == nil {
		return &AppError{Step: "validate configuration", Err: ErrConfigNull,
			Msg: "invalid configuration"}
	}

	// this is checked again in hsu
	if config.Identity < IdentityStart || config.Identity > IdentityEnd {
		return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
			Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
	}

	if err := config.SessionBus.CheckInterfaces("session"); err != nil {
		return err
	}
	if err := config.SystemBus.CheckInterfaces("system"); err != nil {
		return err
	}

	if config.Container == nil {
		return &AppError{Step: "validate configuration", Err: ErrConfigNull,
			Msg: "configuration missing container state"}
	}
	if config.Container.Home == nil {
		return &AppError{Step: "validate configuration", Err: ErrConfigNull,
			Msg: "container configuration missing path to home directory"}
	}
	if config.Container.Shell == nil {
		return &AppError{Step: "validate configuration", Err: ErrConfigNull,
			Msg: "container configuration missing path to shell"}
	}
	if config.Container.Path == nil {
		return &AppError{Step: "validate configuration", Err: ErrConfigNull,
			Msg: "container configuration missing path to initial program"}
	}

	for key := range config.Container.Env {
		if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
			return &AppError{Step: "validate configuration", Err: ErrEnviron,
				Msg: "invalid environment variable " + strconv.Quote(key)}
		}
	}

	if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
		return &AppError{Step: "validate configuration", Err: ErrInsecure,
			Msg: "enablement PulseAudio is insecure and no longer supported"}
	}

	return nil
}

// ExtraPermConfig describes an acl update to perform before setuid.
type ExtraPermConfig struct {
	// Whether to create Path as a directory if it does not exist.
	Ensure bool `json:"ensure,omitempty"`
	// Pathname to act on.
	Path *check.Absolute `json:"path"`
	// Whether to set ACL_READ for the target user.
	Read bool `json:"r,omitempty"`
	// Whether to set ACL_WRITE for the target user.
	Write bool `json:"w,omitempty"`
	// Whether to set ACL_EXECUTE for the target user.
	Execute bool `json:"x,omitempty"`
}

// String returns a checked string representation of [ExtraPermConfig].
func (e *ExtraPermConfig) String() string {
	if e == nil || e.Path == nil {
		return "<invalid>"
	}
	buf := make([]byte, 0, 5+len(e.Path.String()))
	buf = append(buf, '-', '-', '-')
	if e.Ensure {
		buf = append(buf, '+')
	}
	buf = append(buf, ':')
	buf = append(buf, []byte(e.Path.String())...)
	if e.Read {
		buf[0] = 'r'
	}
	if e.Write {
		buf[1] = 'w'
	}
	if e.Execute {
		buf[2] = 'x'
	}
	return string(buf)
}