Allow global network allowlist wildcard (#15549)

## Problem

Today `codex-network-proxy` rejects a global `*` in
`network.allowed_domains`, so there is no static way to configure a
denylist-only posture for public hosts. Users have to enumerate broad
allowlist patterns instead.

## Approach

- Make global wildcard acceptance field-specific: `allowed_domains` can
use `*`, while `denied_domains` still rejects a global wildcard.
- Keep the existing evaluation order, so explicit denies still win first
and local/private protections still apply unless separately enabled.
- Add coverage for the denylist-only behavior and update the README to
document it.

## Validation

- `just fmt`
- `cargo test -p codex-network-proxy` (full run had one unrelated flaky
telemetry test:
`network_policy::tests::emit_block_decision_audit_event_emits_non_domain_event`;
reran in isolation and it passed)
- `cargo test -p codex-network-proxy
network_policy::tests::emit_block_decision_audit_event_emits_non_domain_event
-- --exact --nocapture`
- `just fix -p codex-network-proxy`
- `just argument-comment-lint`
This commit is contained in:
rreichel3-oai
2026-03-24 10:43:46 -04:00
committed by GitHub
parent 95e1d59939
commit 1db6cb9789
4 changed files with 108 additions and 35 deletions

View File

@@ -2,6 +2,7 @@
use crate::config::NetworkMode;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use anyhow::ensure;
use globset::GlobBuilder;
use globset::GlobSet;
@@ -151,19 +152,38 @@ pub(crate) fn is_global_wildcard_domain_pattern(pattern: &str) -> bool {
.any(|candidate| candidate == "*")
}
pub(crate) fn compile_globset(patterns: &[String]) -> Result<GlobSet> {
#[derive(Clone, Copy, PartialEq, Eq)]
enum GlobalWildcard {
Allow,
Reject,
}
pub(crate) fn compile_allowlist_globset(patterns: &[String]) -> Result<GlobSet> {
compile_globset_with_policy(patterns, GlobalWildcard::Allow)
}
pub(crate) fn compile_denylist_globset(patterns: &[String]) -> Result<GlobSet> {
compile_globset_with_policy(patterns, GlobalWildcard::Reject)
}
fn compile_globset_with_policy(
patterns: &[String],
global_wildcard: GlobalWildcard,
) -> Result<GlobSet> {
let mut builder = GlobSetBuilder::new();
let mut seen = HashSet::new();
for pattern in patterns {
ensure!(
!is_global_wildcard_domain_pattern(pattern),
"unsupported global wildcard domain pattern \"*\"; use exact hosts or scoped wildcards like *.example.com or **.example.com"
);
if global_wildcard == GlobalWildcard::Reject && is_global_wildcard_domain_pattern(pattern) {
bail!(
"unsupported global wildcard domain pattern \"*\"; use exact hosts or scoped wildcards like *.example.com or **.example.com"
);
}
let pattern = normalize_pattern(pattern);
// Supported domain patterns:
// - "example.com": match the exact host
// - "*.example.com": match any subdomain (not the apex)
// - "**.example.com": match the apex and any subdomain
// - "*": match every host when explicitly enabled for allowlist compilation
for candidate in expand_domain_pattern(&pattern) {
if !seen.insert(candidate.clone()) {
continue;
@@ -333,7 +353,7 @@ mod tests {
#[test]
fn compile_globset_normalizes_trailing_dots() {
let set = compile_globset(&["Example.COM.".to_string()]).unwrap();
let set = compile_denylist_globset(&["Example.COM.".to_string()]).unwrap();
assert_eq!(true, set.is_match("example.com"));
assert_eq!(false, set.is_match("api.example.com"));
@@ -341,7 +361,7 @@ mod tests {
#[test]
fn compile_globset_normalizes_wildcards() {
let set = compile_globset(&["*.Example.COM.".to_string()]).unwrap();
let set = compile_denylist_globset(&["*.Example.COM.".to_string()]).unwrap();
assert_eq!(true, set.is_match("api.example.com"));
assert_eq!(false, set.is_match("example.com"));
@@ -349,7 +369,7 @@ mod tests {
#[test]
fn compile_globset_normalizes_apex_and_subdomains() {
let set = compile_globset(&["**.Example.COM.".to_string()]).unwrap();
let set = compile_denylist_globset(&["**.Example.COM.".to_string()]).unwrap();
assert_eq!(true, set.is_match("example.com"));
assert_eq!(true, set.is_match("api.example.com"));
@@ -357,7 +377,7 @@ mod tests {
#[test]
fn compile_globset_normalizes_bracketed_ipv6_literals() {
let set = compile_globset(&["[::1]".to_string()]).unwrap();
let set = compile_denylist_globset(&["[::1]".to_string()]).unwrap();
assert_eq!(true, set.is_match("::1"));
}