@@ -2,6 +2,7 @@ package cli
22
33import (
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
2627type 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 {
5864func 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
284329func 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 }
0 commit comments