Skip to content

Commit e400285

Browse files
committed
remove trailing * support for hostname
1 parent 9e92f3c commit e400285

File tree

3 files changed

+250
-51
lines changed

3 files changed

+250
-51
lines changed

README.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | ba
2525

2626
```bash
2727
# Allow only requests to github.com
28-
boundary --allow "github.com" -- curl https://github.com
28+
boundary --allow "domain=github.com" -- curl https://github.com
2929

3030
# Allow full access to GitHub issues API, but only GET/HEAD elsewhere on GitHub
3131
boundary \
32-
--allow "github.com/api/issues/*" \
33-
--allow "GET,HEAD github.com" \
32+
--allow "domain=github.com path=/api/issues/*" \
33+
--allow "method=GET,HEAD domain=github.com" \
3434
-- npm install
3535

3636
# Default deny-all: everything is blocked unless explicitly allowed
@@ -41,25 +41,29 @@ boundary -- curl https://example.com
4141

4242
### Format
4343
```text
44-
--allow "pattern" # All HTTP methods allowed
45-
--allow "METHOD[,METHOD] pattern" # Specific methods only
44+
--allow "key=value [key=value ...]"
4645
```
4746

47+
**Keys:**
48+
- `method` - HTTP method(s), comma-separated (GET, POST, etc.)
49+
- `domain` - Domain/hostname pattern
50+
- `path` - URL path pattern
51+
4852
### Examples
4953
```bash
50-
boundary --allow "github.com" -- git pull
51-
boundary --allow "*.github.com" -- npm install # GitHub subdomains
52-
boundary --allow "api.*" -- ./app # Any API domain
53-
boundary --allow "GET,HEAD api.github.com" -- curl https://api.github.com
54+
boundary --allow "domain=github.com" -- git pull
55+
boundary --allow "domain=*.github.com" -- npm install # GitHub subdomains
56+
boundary --allow "method=GET,HEAD domain=api.github.com" -- curl https://api.github.com
57+
boundary --allow "method=POST domain=api.example.com path=/users" -- ./app
5458
```
5559

5660
Wildcards: `*` matches any characters. All traffic is denied unless explicitly allowed.
5761

5862
## Logging
5963

6064
```bash
61-
boundary --log-level info --allow "*" -- npm install # Show all requests
62-
boundary --log-level debug --allow "github.com" -- git pull # Debug info
65+
boundary --log-level info --allow "method=*" -- npm install # Show all requests
66+
boundary --log-level debug --allow "domain=github.com" -- git pull # Debug info
6367
```
6468

6569
**Log Levels:** `error`, `warn` (default), `info`, `debug`
@@ -70,10 +74,10 @@ When you can't or don't want to run with sudo privileges, use `--unprivileged`:
7074

7175
```bash
7276
# Run without network isolation (uses HTTP_PROXY/HTTPS_PROXY environment variables)
73-
boundary --unprivileged --allow "github.com" -- npm install
77+
boundary --unprivileged --allow "domain=github.com" -- npm install
7478

7579
# Useful in containers or restricted environments
76-
boundary --unprivileged --allow "*.npmjs.org" --allow "registry.npmjs.org" -- npm install
80+
boundary --unprivileged --allow "domain=*.npmjs.org" --allow "domain=registry.npmjs.org" -- npm install
7781
```
7882

7983
**Unprivileged Mode:**

rules/rules.go

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ func parseHostPattern(input string) (host []labelPattern, rest string, err error
118118
host = append(host, label)
119119
}
120120

121+
// Validate: host patterns cannot end with asterisk
122+
if len(host) > 0 && host[len(host)-1] == "*" {
123+
return nil, "", errors.New("host patterns cannot end with asterisk")
124+
}
125+
121126
return host, rest, nil
122127
}
123128

@@ -319,17 +324,31 @@ func parseAllowRule(ruleStr string) (Rule, error) {
319324
// Parse the value based on the key type
320325
switch key {
321326
case "method":
322-
token, remaining, err := parseMethodPattern(valueRest)
323-
if err != nil {
324-
return Rule{}, fmt.Errorf("failed to parse method: %v", err)
325-
}
327+
// Handle comma-separated methods
328+
methodsRest := valueRest
326329

327330
// Initialize Methods map if needed
328331
if rule.MethodPatterns == nil {
329332
rule.MethodPatterns = make(map[methodPattern]struct{})
330333
}
331-
rule.MethodPatterns[token] = struct{}{}
332-
rest = remaining
334+
335+
for {
336+
token, remaining, err := parseMethodPattern(methodsRest)
337+
if err != nil {
338+
return Rule{}, fmt.Errorf("failed to parse method: %v", err)
339+
}
340+
341+
rule.MethodPatterns[token] = struct{}{}
342+
343+
// Check if there's a comma for more methods
344+
if remaining != "" && remaining[0] == ',' {
345+
methodsRest = remaining[1:] // Skip the comma
346+
continue
347+
}
348+
349+
rest = remaining
350+
break
351+
}
333352

334353
case "domain":
335354
hostLabels, remaining, err := parseHostPattern(valueRest)
@@ -429,14 +448,14 @@ func (re *Engine) matches(r Rule, method, url string) bool {
429448
}
430449
}
431450
if !methodMatches {
432-
re.logger.Info("rule does not match", "reason", "method pattern mismatch", "rule", r.Raw, "method", method, "url", url)
451+
re.logger.Debug("rule does not match", "reason", "method pattern mismatch", "rule", r.Raw, "method", method, "url", url)
433452
return false
434453
}
435454
}
436455

437456
parsedUrl, err := neturl.Parse(url)
438457
if err != nil {
439-
re.logger.Info("rule does not match", "reason", "invalid URL", "rule", r.Raw, "method", method, "url", url, "error", err)
458+
re.logger.Debug("rule does not match", "reason", "invalid URL", "rule", r.Raw, "method", method, "url", url, "error", err)
440459
return false
441460
}
442461

@@ -450,15 +469,16 @@ func (re *Engine) matches(r Rule, method, url string) bool {
450469

451470
// If the host pattern is longer than the actual host, it's definitely not a match
452471
if len(r.HostPattern) > len(labels) {
453-
re.logger.Info("rule does not match", "reason", "host pattern too long", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.HostPattern), "hostname_labels", len(labels))
472+
re.logger.Debug("rule does not match", "reason", "host pattern too long", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.HostPattern), "hostname_labels", len(labels))
454473
return false
455474
}
456475

457-
// Compare pattern with the end of labels (allowing subdomains)
476+
// Since host patterns cannot end with asterisk, we only need to handle:
477+
// "example.com" or "*.example.com" - match from the end (allowing subdomains)
458478
for i, lp := range r.HostPattern {
459479
labelIndex := len(labels) - len(r.HostPattern) + i
460480
if string(lp) != labels[labelIndex] && lp != "*" {
461-
re.logger.Info("rule does not match", "reason", "host pattern label mismatch", "rule", r.Raw, "method", method, "url", url, "expected", string(lp), "actual", labels[labelIndex])
481+
re.logger.Debug("rule does not match", "reason", "host pattern label mismatch", "rule", r.Raw, "method", method, "url", url, "expected", string(lp), "actual", labels[labelIndex])
462482
return false
463483
}
464484
}
@@ -467,21 +487,26 @@ func (re *Engine) matches(r Rule, method, url string) bool {
467487
if r.PathPattern != nil {
468488
segments := strings.Split(parsedUrl.Path, "/")
469489

490+
// Skip the first empty segment if the path starts with "/"
491+
if len(segments) > 0 && segments[0] == "" {
492+
segments = segments[1:]
493+
}
494+
470495
// If the path pattern is longer than the actual path, definitely not a match
471496
if len(r.PathPattern) > len(segments) {
472-
re.logger.Info("rule does not match", "reason", "path pattern too long", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.PathPattern), "path_segments", len(segments))
497+
re.logger.Debug("rule does not match", "reason", "path pattern too long", "rule", r.Raw, "method", method, "url", url, "pattern_length", len(r.PathPattern), "path_segments", len(segments))
473498
return false
474499
}
475500

476501
// Each segment in the pattern must be either as asterisk or match the actual path segment
477502
for i, sp := range r.PathPattern {
478503
if string(sp) != segments[i] && sp != "*" {
479-
re.logger.Info("rule does not match", "reason", "path pattern segment mismatch", "rule", r.Raw, "method", method, "url", url, "expected", string(sp), "actual", segments[i])
504+
re.logger.Debug("rule does not match", "reason", "path pattern segment mismatch", "rule", r.Raw, "method", method, "url", url, "expected", string(sp), "actual", segments[i])
480505
return false
481506
}
482507
}
483508
}
484509

485-
re.logger.Info("rule matches", "reason", "all patterns matched", "rule", r.Raw, "method", method, "url", url)
510+
re.logger.Debug("rule matches", "reason", "all patterns matched", "rule", r.Raw, "method", method, "url", url)
486511
return true
487512
}

0 commit comments

Comments
 (0)