Skip to content

Commit cb50342

Browse files
feat: initial implementation of config file support (#79)
* feat: initial implementation of config file support
1 parent 143c628 commit cb50342

File tree

2 files changed

+73
-27
lines changed

2 files changed

+73
-27
lines changed

cli/cli.go

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"log"
78
"log/slog"
@@ -24,12 +25,14 @@ import (
2425

2526
// Config holds all configuration for the CLI
2627
type Config struct {
27-
AllowStrings []string
28-
LogLevel string
29-
LogDir string
30-
ProxyPort int64
31-
PprofEnabled bool
32-
PprofPort int64
28+
Config serpent.YAMLConfigPath `yaml:"-"`
29+
AllowListStrings serpent.StringArray `yaml:"allowlist"` // From config file
30+
AllowStrings serpent.StringArray `yaml:"-"` // From CLI flags only
31+
LogLevel serpent.String `yaml:"log_level"`
32+
LogDir serpent.String `yaml:"log_dir"`
33+
ProxyPort serpent.Int64 `yaml:"proxy_port"`
34+
PprofEnabled serpent.Bool `yaml:"pprof_enabled"`
35+
PprofPort serpent.Int64 `yaml:"pprof_port"`
3336
}
3437

3538
// NewCommand creates and returns the root serpent command
@@ -47,6 +50,9 @@ func NewCommand() *serpent.Command {
4750
# Monitor all requests to specific domains (allow only those)
4851
boundary --allow "domain=github.com path=/api/issues/*" --allow "method=GET,HEAD domain=github.com" -- npm install
4952
53+
# Use allowlist from config file with additional CLI allow rules
54+
boundary --allow "domain=example.com" -- curl https://example.com
55+
5056
# Block everything by default (implicit)`
5157

5258
return cmd
@@ -58,49 +64,76 @@ func NewCommand() *serpent.Command {
5864
func BaseCommand() *serpent.Command {
5965
config := Config{}
6066

67+
// Set default config path if file exists - serpent will load it automatically
68+
if home, err := os.UserHomeDir(); err == nil {
69+
defaultPath := filepath.Join(home, ".config", "coder_boundary", "config.yaml")
70+
if _, err := os.Stat(defaultPath); err == nil {
71+
config.Config = serpent.YAMLConfigPath(defaultPath)
72+
}
73+
}
74+
6175
return &serpent.Command{
6276
Use: "boundary",
6377
Short: "Network isolation tool for monitoring and restricting HTTP/HTTPS requests",
6478
Long: `boundary creates an isolated network environment for target processes, intercepting HTTP/HTTPS traffic through a transparent proxy that enforces user-defined allow rules.`,
6579
Options: []serpent.Option{
80+
{
81+
Flag: "config",
82+
Env: "BOUNDARY_CONFIG",
83+
Description: "Path to YAML config file.",
84+
Value: &config.Config,
85+
YAML: "",
86+
},
6687
{
6788
Flag: "allow",
6889
Env: "BOUNDARY_ALLOW",
69-
Description: "Allow rule (repeatable). Format: \"pattern\" or \"METHOD[,METHOD] pattern\".",
70-
Value: serpent.StringArrayOf(&config.AllowStrings),
90+
Description: "Allow rule (repeatable). These are merged with allowlist from config file. Format: \"pattern\" or \"METHOD[,METHOD] pattern\".",
91+
Value: &config.AllowStrings,
92+
YAML: "", // CLI only, not loaded from YAML
93+
},
94+
{
95+
Flag: "", // No CLI flag, YAML only
96+
Description: "Allowlist rules from config file (YAML only).",
97+
Value: &config.AllowListStrings,
98+
YAML: "allowlist",
7199
},
72100
{
73101
Flag: "log-level",
74102
Env: "BOUNDARY_LOG_LEVEL",
75103
Description: "Set log level (error, warn, info, debug).",
76104
Default: "warn",
77-
Value: serpent.StringOf(&config.LogLevel),
105+
Value: &config.LogLevel,
106+
YAML: "log_level",
78107
},
79108
{
80109
Flag: "log-dir",
81110
Env: "BOUNDARY_LOG_DIR",
82111
Description: "Set a directory to write logs to rather than stderr.",
83-
Value: serpent.StringOf(&config.LogDir),
112+
Value: &config.LogDir,
113+
YAML: "log_dir",
84114
},
85115
{
86116
Flag: "proxy-port",
87117
Env: "PROXY_PORT",
88118
Description: "Set a port for HTTP proxy.",
89119
Default: "8080",
90-
Value: serpent.Int64Of(&config.ProxyPort),
120+
Value: &config.ProxyPort,
121+
YAML: "proxy_port",
91122
},
92123
{
93124
Flag: "pprof",
94125
Env: "BOUNDARY_PPROF",
95126
Description: "Enable pprof profiling server.",
96-
Value: serpent.BoolOf(&config.PprofEnabled),
127+
Value: &config.PprofEnabled,
128+
YAML: "pprof_enabled",
97129
},
98130
{
99131
Flag: "pprof-port",
100132
Env: "BOUNDARY_PPROF_PORT",
101133
Description: "Set port for pprof profiling server.",
102134
Default: "6060",
103-
Value: serpent.Int64Of(&config.PprofPort),
135+
Value: &config.PprofPort,
136+
YAML: "pprof_port",
104137
},
105138
},
106139
Handler: func(inv *serpent.Invocation) error {
@@ -121,6 +154,12 @@ func Run(ctx context.Context, config Config, args []string) error {
121154
return fmt.Errorf("could not set up logging: %v", err)
122155
}
123156

157+
configInJSON, err := json.Marshal(config)
158+
if err != nil {
159+
return err
160+
}
161+
logger.Debug("config", "json_config", configInJSON)
162+
124163
if isChild() {
125164
logger.Info("boundary CHILD process is started")
126165

@@ -158,13 +197,19 @@ func Run(ctx context.Context, config Config, args []string) error {
158197
return fmt.Errorf("no command specified")
159198
}
160199

161-
// Parse allow list; default to deny-all if none provided
162-
if len(config.AllowStrings) == 0 {
200+
// Merge allowlist from config file with allow from CLI flags
201+
allowListStrings := config.AllowListStrings.Value()
202+
allowStrings := config.AllowStrings.Value()
203+
204+
// Combine allowlist (config file) with allow (CLI flags)
205+
allAllowStrings := append(allowListStrings, allowStrings...)
206+
207+
if len(allAllowStrings) == 0 {
163208
logger.Warn("No allow rules specified; all network traffic will be denied by default")
164209
}
165210

166211
// Parse allow rules
167-
allowRules, err := rulesengine.ParseAllowSpecs(config.AllowStrings)
212+
allowRules, err := rulesengine.ParseAllowSpecs(allAllowStrings)
168213
if err != nil {
169214
logger.Error("Failed to parse allow rules", "error", err)
170215
return fmt.Errorf("failed to parse allow rules: %v", err)
@@ -197,7 +242,7 @@ func Run(ctx context.Context, config Config, args []string) error {
197242
// Create jailer with cert path from TLS setup
198243
jailer, err := createJailer(jail.Config{
199244
Logger: logger,
200-
HttpProxyPort: int(config.ProxyPort),
245+
HttpProxyPort: int(config.ProxyPort.Value()),
201246
Username: username,
202247
Uid: uid,
203248
Gid: gid,
@@ -216,9 +261,9 @@ func Run(ctx context.Context, config Config, args []string) error {
216261
TLSConfig: tlsConfig,
217262
Logger: logger,
218263
Jailer: jailer,
219-
ProxyPort: int(config.ProxyPort),
220-
PprofEnabled: config.PprofEnabled,
221-
PprofPort: int(config.PprofPort),
264+
ProxyPort: int(config.ProxyPort.Value()),
265+
PprofEnabled: config.PprofEnabled.Value(),
266+
PprofPort: int(config.PprofPort.Value()),
222267
})
223268
if err != nil {
224269
return fmt.Errorf("failed to create boundary instance: %v", err)
@@ -283,7 +328,7 @@ func Run(ctx context.Context, config Config, args []string) error {
283328
// setupLogging creates a slog logger with the specified level
284329
func setupLogging(config Config) (*slog.Logger, error) {
285330
var level slog.Level
286-
switch strings.ToLower(config.LogLevel) {
331+
switch strings.ToLower(config.LogLevel.Value()) {
287332
case "error":
288333
level = slog.LevelError
289334
case "warn":
@@ -298,18 +343,19 @@ func setupLogging(config Config) (*slog.Logger, error) {
298343

299344
logTarget := os.Stderr
300345

301-
if config.LogDir != "" {
346+
logDir := config.LogDir.Value()
347+
if logDir != "" {
302348
// Set up the logging directory if it doesn't exist yet
303-
if err := os.MkdirAll(config.LogDir, 0755); err != nil {
304-
return nil, fmt.Errorf("could not set up log dir %s: %v", config.LogDir, err)
349+
if err := os.MkdirAll(logDir, 0755); err != nil {
350+
return nil, fmt.Errorf("could not set up log dir %s: %v", logDir, err)
305351
}
306352

307353
// Create a logfile (timestamp and pid to avoid race conditions with multiple boundary calls running)
308354
logFilePath := fmt.Sprintf("boundary-%s-%d.log",
309355
time.Now().Format("2006-01-02_15-04-05"),
310356
os.Getpid())
311357

312-
logFile, err := os.Create(filepath.Join(config.LogDir, logFilePath))
358+
logFile, err := os.Create(filepath.Join(logDir, logFilePath))
313359
if err != nil {
314360
return nil, fmt.Errorf("could not create log file %s: %v", logFilePath, err)
315361
}

rulesengine/rules.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,14 @@ func parseMethodPattern(token string) (string, string, error) {
142142
if token == "" {
143143
return "", "", errors.New("expected http token, got empty string")
144144
}
145-
145+
146146
// Find the first invalid HTTP token character
147147
for i := 0; i < len(token); i++ {
148148
if !isHTTPTokenChar(token[i]) {
149149
return token[:i], token[i:], nil
150150
}
151151
}
152-
152+
153153
// Entire string is a valid HTTP token
154154
return token, "", nil
155155
}

0 commit comments

Comments
 (0)