Skip to content

Commit b7434a8

Browse files
crystalinclaude
andauthored
feat(proxy): Add wildcard subdomain support with PSL integration (#137)
* feat(proxy): add wildcard subdomain support with PSL integration - Add cross-platform wildcard credential resolution using _wildcard. prefix - Integrate Public Suffix List (PSL) for security boundaries - Implement domain normalization with IDN support (punycode) - Add TTL-based caching with size limits (10K entries max) - Add feature flag CNP_WILDCARD_CREDENTIALS with shadow mode - Fix critical path traversal vulnerability in credential resolution - Add comprehensive unit tests and security tests - Create ADR-023 documenting the implementation - Update environment variables documentation This allows organizations to use a single credential file for multiple subdomains while maintaining security through PSL boundaries and proper path validation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * chore: remove temporary planning files Remove IMPORTANT_FILES.yaml and PLAN.md that were used for implementation planning but should not be part of the codebase. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * docs: update documentation for wildcard credentials - Add wildcard credential environment variables to .env.example - Update main README with wildcard credentials section - Add wildcard support details to proxy README - Update credentials README with wildcard file naming 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 6789e73 commit b7434a8

File tree

10 files changed

+1032
-87
lines changed

10 files changed

+1032
-87
lines changed

.env.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ DASHBOARD_API_KEY=your-secure-dashboard-key-here
2323
# Directory for domain credential files (default: ./credentials)
2424
CREDENTIALS_DIR=./credentials
2525

26+
# Wildcard credential support (default: false)
27+
# true = Enable wildcard matching, false = Disable, shadow = Log only (no behavior change)
28+
# When enabled, _wildcard.example.com.credentials.json matches all subdomains of example.com
29+
CNP_WILDCARD_CREDENTIALS=false
30+
31+
# TTL for credential resolution cache in milliseconds (default: 300000 = 5 minutes)
32+
# Caches both successful and failed credential lookups for performance
33+
CNP_RESOLUTION_CACHE_TTL=300000
34+
35+
# Enable debug logging for credential resolution (default: false)
36+
# Logs detailed information about domain matching and wildcard resolution
37+
CNP_DEBUG_RESOLUTION=false
38+
2639
# ===================
2740
# Service Configuration
2841
# ===================

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,27 @@ EOF
318318

319319
(_Use `credentials/localhost\:3000.credentials.json` for using it locally_)
320320

321+
#### Wildcard Credentials
322+
323+
You can use wildcard credentials to match multiple subdomains with a single credential file:
324+
325+
```bash
326+
# Matches all subdomains of example.com (api.example.com, staging.example.com, etc.)
327+
cat > credentials/_wildcard.example.com.credentials.json << EOF
328+
{
329+
"type": "api_key",
330+
"accountId": "acc_name_to_display",
331+
"api_key": "sk-ant-...",
332+
"client_api_key": "cnp_live_..."
333+
}
334+
EOF
335+
336+
# Enable wildcard support
337+
export CNP_WILDCARD_CREDENTIALS=true
338+
```
339+
340+
Note: Exact matches take precedence over wildcards. See [ADR-023](docs/04-Architecture/ADRs/adr-023-wildcard-subdomain-support.md) for details.
341+
321342
Authenticate your credential with Claude MAX Plan:
322343

323344
```bash

bun.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"nanoid": "^5.0.8",
8181
"node-cache": "^5.1.2",
8282
"p-limit": "^6.1.0",
83+
"psl": "^1.15.0",
8384
"rate-limiter-flexible": "^5.0.3",
8485
"redact-pii": "^3.4.0",
8586
"zod": "^3.24.1",
@@ -88,6 +89,7 @@
8889
"@types/bun": "^1.2.17",
8990
"@types/js-yaml": "^4.0.9",
9091
"@types/node": "^24.0.10",
92+
"@types/psl": "^1.11.0",
9193
},
9294
},
9395
},
@@ -238,6 +240,8 @@
238240

239241
"@types/pino": ["@types/[email protected]", "", { "dependencies": { "pino": "*" } }, "sha512-wKoab31pknvILkxAF8ss+v9iNyhw5Iu/0jLtRkUD74cNfOOLJNnqfFKAv0r7wVaTQxRZtWrMpGfShwwBjOcgcg=="],
240242

243+
"@types/psl": ["@types/[email protected]", "", { "dependencies": { "psl": "*" } }, "sha512-OeASqmnreaSJyxwljqCZjRIf7jpbwswaqbSPahE99PZrizNAOXEl9HRWhXvCt3fkAc/WuF8wcZ6zkDHXaE7X4g=="],
244+
241245
"@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
242246

243247
"@types/rimraf": ["@types/[email protected]", "", { "dependencies": { "@types/glob": "*", "@types/node": "*" } }, "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ=="],
@@ -784,6 +788,8 @@
784788

785789
"proxy-from-env": ["[email protected]", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
786790

791+
"psl": ["[email protected]", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="],
792+
787793
"punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
788794

789795
"punycode.js": ["[email protected]", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],

credentials/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ Each domain should have its own credential file named `<domain>.credentials.json
88

99
Example: `example.com.credentials.json`
1010

11+
### Wildcard Credentials
12+
13+
You can create wildcard credential files to match multiple subdomains:
14+
15+
- `_wildcard.example.com.credentials.json` - Matches all subdomains of example.com
16+
- `_wildcard.staging.example.com.credentials.json` - Matches all subdomains of staging.example.com
17+
18+
**Note**: Exact matches always take precedence over wildcards. Enable with `CNP_WILDCARD_CREDENTIALS=true`.
19+
1120
## Credential Structure
1221

1322
```json
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# ADR-023: Wildcard Subdomain Support
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
The Claude Nexus Proxy needs to support organizations with many subdomains (e.g., `api.staging.example.com`, `api.prod.example.com`, `api.dev.example.com`) without requiring separate credential files for each subdomain. This is particularly important for:
10+
11+
1. Large organizations with environment-based subdomains
12+
2. Multi-tenant SaaS applications with customer-specific subdomains
13+
3. Development teams using feature branch deployments
14+
15+
Currently, each subdomain requires its own credential file, leading to:
16+
17+
- Credential proliferation and management overhead
18+
- Increased risk of configuration drift
19+
- Difficulty in rotating API keys across environments
20+
21+
## Decision
22+
23+
We will implement wildcard subdomain support using a prefix-based approach that is cross-platform compatible:
24+
25+
### 1. Wildcard Credential Files
26+
27+
- Use `_wildcard.` prefix for wildcard credential files
28+
- Example: `_wildcard.example.com.credentials.json` matches `*.example.com`
29+
- This avoids filesystem issues with `*` character on Windows
30+
31+
### 2. Resolution Order
32+
33+
Credentials are resolved in order of specificity:
34+
35+
1. Exact match: `api.staging.example.com.credentials.json`
36+
2. Most specific wildcard: `_wildcard.staging.example.com.credentials.json`
37+
3. Less specific wildcard: `_wildcard.example.com.credentials.json`
38+
4. No match: fallback to default behavior
39+
40+
### 3. Security Boundaries
41+
42+
- Integrate Public Suffix List (PSL) to prevent privilege escalation
43+
- Wildcard matching stops at registrable domain boundaries
44+
- Example: `_wildcard.co.uk.credentials.json` will NOT match `example.co.uk`
45+
46+
### 4. Feature Flag Control
47+
48+
- Environment variable: `CNP_WILDCARD_CREDENTIALS`
49+
- Values:
50+
- `true`: Enable wildcard support
51+
- `false`: Disable (default)
52+
- `shadow`: Log matches without changing behavior
53+
54+
### 5. Performance Optimization
55+
56+
- TTL-based caching for credential resolution
57+
- Cache both positive and negative results
58+
- Environment variable: `CNP_RESOLUTION_CACHE_TTL` (default: 5 minutes)
59+
- Maximum cache size: 10,000 entries with LRU eviction
60+
61+
### 6. Domain Normalization
62+
63+
- Convert to lowercase
64+
- Remove ports
65+
- Handle Internationalized Domain Names (IDN) with punycode
66+
- Remove trailing dots
67+
- Collapse consecutive dots
68+
69+
## Consequences
70+
71+
### Positive
72+
73+
- **Simplified credential management**: One file can serve multiple subdomains
74+
- **Reduced configuration drift**: Centralized credential updates
75+
- **Better security**: PSL integration prevents unauthorized access
76+
- **Cross-platform compatibility**: Works on all operating systems
77+
- **Performance**: Caching reduces filesystem operations
78+
- **Backward compatible**: Existing configurations continue to work
79+
- **Safe rollout**: Shadow mode allows testing without risk
80+
81+
### Negative
82+
83+
- **Added complexity**: More code paths to maintain
84+
- **Memory usage**: Cache requires memory (bounded at 10K entries)
85+
- **PSL dependency**: Requires maintaining PSL library
86+
- **Learning curve**: Administrators need to understand wildcard behavior
87+
88+
### Neutral
89+
90+
- **Explicit wildcards**: Requires `_wildcard.` prefix (not automatic)
91+
- **Cache invalidation**: Manual cache clear may be needed for immediate updates
92+
93+
## Implementation Details
94+
95+
### Key Components
96+
97+
1. **AuthenticationService**: Enhanced with wildcard resolution logic
98+
2. **Domain normalization**: Handles IDN, ports, and edge cases
99+
3. **PSL integration**: Prevents privilege escalation attacks
100+
4. **Resolution cache**: Map with TTL-based expiration
101+
5. **Feature flags**: Safe rollout with shadow mode
102+
103+
### Configuration Examples
104+
105+
```
106+
# Wildcard for all staging subdomains
107+
_wildcard.staging.example.com.credentials.json
108+
109+
# Wildcard for all subdomains of example.com
110+
_wildcard.example.com.credentials.json
111+
112+
# Exact match (takes precedence)
113+
api.staging.example.com.credentials.json
114+
```
115+
116+
### Environment Variables
117+
118+
- `CNP_WILDCARD_CREDENTIALS`: Enable/disable feature
119+
- `CNP_RESOLUTION_CACHE_TTL`: Cache TTL in milliseconds
120+
- `CNP_DEBUG_RESOLUTION`: Enable debug logging
121+
122+
## References
123+
124+
- [Public Suffix List](https://publicsuffix.org/)
125+
- [RFC 3490: Internationalizing Domain Names in Applications (IDNA)](https://tools.ietf.org/html/rfc3490)
126+
- Original implementation plan: `/PLAN.md`

docs/06-Reference/environment-vars.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ DATABASE_URL=postgresql://user:password@localhost:5432/claude_nexus
4747
| --------------------- | ----------------------------- | ------- |
4848
| `DASHBOARD_CACHE_TTL` | Dashboard cache TTL (seconds) | `30` |
4949

50+
### Wildcard Credentials
51+
52+
| Variable | Description | Default |
53+
| -------------------------- | ----------------------------------------------------- | -------- |
54+
| `CNP_WILDCARD_CREDENTIALS` | Enable wildcard subdomain support (true/false/shadow) | `false` |
55+
| `CNP_RESOLUTION_CACHE_TTL` | TTL for credential resolution cache (ms) | `300000` |
56+
| `CNP_DEBUG_RESOLUTION` | Enable debug logging for credential resolution | `false` |
57+
5058
## Service Configuration
5159

5260
### Proxy Service
@@ -236,6 +244,11 @@ GEMINI_MODEL_NAME=gemini-2.0-flash-exp
236244
CREDENTIALS_DIR=./credentials
237245
TEST_SAMPLES_DIR=./test-samples
238246

247+
# Wildcard Credentials (optional)
248+
# CNP_WILDCARD_CREDENTIALS=true
249+
# CNP_RESOLUTION_CACHE_TTL=300000
250+
# CNP_DEBUG_RESOLUTION=false
251+
239252
# Production (uncomment for production)
240253
# NODE_ENV=production
241254
# LOG_LEVEL=warn

services/proxy/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ See `.env.example` in the root directory for all available environment variables
4545
- `DATABASE_URL` - PostgreSQL connection
4646
- `STORAGE_ENABLED` - Enable storage (default: false)
4747
- `SLACK_WEBHOOK_URL` - Slack notifications
48+
- `CNP_WILDCARD_CREDENTIALS` - Enable wildcard credential support (true/false/shadow)
49+
- `CNP_RESOLUTION_CACHE_TTL` - TTL for credential resolution cache in ms (default: 300000)
50+
- `CNP_DEBUG_RESOLUTION` - Enable debug logging for credential resolution
4851

4952
## API Endpoints
5053

services/proxy/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@
3232
"nanoid": "^5.0.8",
3333
"node-cache": "^5.1.2",
3434
"p-limit": "^6.1.0",
35-
"zod": "^3.24.1",
35+
"psl": "^1.15.0",
36+
"rate-limiter-flexible": "^5.0.3",
3637
"redact-pii": "^3.4.0",
37-
"rate-limiter-flexible": "^5.0.3"
38+
"zod": "^3.24.1"
3839
},
3940
"devDependencies": {
4041
"@types/bun": "^1.2.17",
42+
"@types/js-yaml": "^4.0.9",
4143
"@types/node": "^24.0.10",
42-
"@types/js-yaml": "^4.0.9"
44+
"@types/psl": "^1.11.0"
4345
},
4446
"engines": {
4547
"node": ">=20.0.0"

0 commit comments

Comments
 (0)