Skip to content

Server profiles (YAML): default-deny allowlists, forced DNS, per-connection logging#1072

Draft
sandinak wants to merge 33 commits intosshuttle:masterfrom
sandinak:feature/server-profiles
Draft

Server profiles (YAML): default-deny allowlists, forced DNS, per-connection logging#1072
sandinak wants to merge 33 commits intosshuttle:masterfrom
sandinak:feature/server-profiles

Conversation

@sandinak
Copy link

This PR introduces a server-side profiles capability loaded from YAML.

  • Adds a server YAML loader from /etc/sshuttle/server.yaml (fallback ~/.config/sshuttle/server.yaml)
  • Default profile (if no config present) routes only 10.4.188.128/25
  • Enforces TCP dest IP and port against allowlists
  • Forces DNS to configured profile nameserver if present
  • Adds per-connection JSONL logging with user, src/dst, decision (MVP logs dst; src tuple protocol extension to follow)
  • Adds client --profile flag to request a named profile (server selects/validates profile)

Next steps: extend wire protocol to include TCP/UDP/DNS source tuple to log accurately and enforce by src if desired; add UDP/DNS enforcement.

This is a draft to facilitate early review and CI.


Pull Request opened by Augment Code with guidance from the PR author

…nly 10.4.188.128/25). Enforce TCP allowlists and log per-connection. Force DNS to configured nameserver if present. (TCP src tuple support to follow)
…r test-bed; test script uses profile=testing and curls node-2
…e key-based fallback; neutralize SSH_AUTH_SOCK/XDG_RUNTIME_DIR in test
@sandinak
Copy link
Author

Pushed a small shell style cleanup: brace variables and consistent quoting across scripts (exec-sshuttle, exec-tool, test-profiles). No functional changes.

Re-ran the containerized profile test locally (scripts/test-profiles): sshuttle connects, node-2 nginx reachable via profile "testing", and server logs show ALLOWED entries as expected.

CI should remain green; happy to address any review nits.

@brianmay
Copy link
Member

brianmay commented Sep 5, 2025

Can you please explain something about the use case here?

/etc/sshuttle/server.yaml is loaded on the server by the server, correct?

@sandinak
Copy link
Author

sandinak commented Sep 6, 2025

So the used case is, we are in an environment where we have a VPN service that constrains to any other VPN services as most vpns do. Our users are starting to use sshuttle to get access to services inside the protected network. We want to enable them to have this capability, but not have them have the ability to have unconstrained access. So the idea here is that this would allow us to set specific access controls from the server side, and allow the users to use this modified version of sshuttle and still maintain security and visibility. I posted the PR as a possibility in case you wanted to include this as a feature long-term.

@sandinak
Copy link
Author

sandinak commented Sep 9, 2025

Updates for server profiles feature:

  1. Default network scope:
  • Server now defaults to the locally attached IPv4 networks from its routing table when a profile omits or leaves allow_nets empty. This removes the hardcoded 10.4.188.128/25 and makes defaults portable.
  • docs/examples/server.yaml updated: default profile now uses empty allow_nets to exercise this behavior.
  1. Documentation:
  • Man page: added --profile option and a new “Server Profiles” section documenting YAML configuration, defaults, and usage.
  • docs/usage.rst: added a “Server-side Profiles” section covering config location, profile selection, and how to disable via env/config.
  • docs/examples/README.md: note about empty allow_nets defaulting to local routes.
  1. Build/feature toggle:
  • Introduced a feature toggle for server profiles: environment variable SSHUTTLE_ENABLE_SERVER_PROFILES (defaults enabled). Also respects an optional top-level profiles_enabled: true|false key in server.yaml. No build-time changes required; runtime toggle avoids packaging complexity.
  1. Server process identification:
  • Server process now annotates its process title when profiles are enabled (e.g., sshuttle-server [profiles=on name=testing]). Uses setproctitle if available; otherwise attempts to write /proc/self/comm on Linux. This is best-effort and non-fatal if unsupported.
  1. Tests:
  • Python unit tests pass locally (73 passed). Lint passes. Containerized E2E test could not be re-run on this machine due to Docker not running, but the test script remains unchanged aside from docs/examples config update. Previous runs had succeeded; behavior should be unchanged functionally with new defaults (testing profile still explicitly allows 10.55.0.0/16 and ports 8080/5001).

Happy to iterate if you prefer a build-time flag instead of the runtime toggle, or if you want the process title change guarded behind a separate opt-in.

…fig; if profile is requested but no config, fail with clear error; log via standard server logging when unprofiled; docs: add behavior and manpage/usage updates
@sandinak
Copy link
Author

sandinak commented Sep 9, 2025

Local E2E results (Docker test-bed)

Positive path

  • Ran scripts/test-profiles with --profile testing
  • Result: success (exit 0)
  • Observed in server logs (syslog style): many action=ALLOWED proto=TCP user=test profile=testing ... dpt=8080 entries
  • This confirms: with server.yaml present, the requested profile is enforced and connections are logged as designed

Negative path (no server config)

  • Removed /etc/sshuttle/server.yaml from the test server and re-ran with --profile testing
  • Result: failure as expected
  • Server error shown to client:
    fatal: Server-side profiles requested by client but no server configuration found. Create a YAML/JSON config at one of: /etc/sshuttle/server.yaml, ~/.config/sshuttle/server.yaml with a 'profiles' section.
  • Client exits with status 99 and prints: fatal: server died with error code 99

Conclusion: Behavior matches the intended semantics:

  • If no server config exists: normal operation (no enforcement), standard server logging for connections
  • If config exists: enforce profiles and log as designed
  • If a client requests --profile but the server has no config: fail with a clear, actionable error

If you’d like any tweaks to the error wording or additional fields in the syslog-style output, happy to adjust.

@sandinak
Copy link
Author

Added a top-level examples/ directory with server-side configuration samples:

  • examples/server-basic.yaml — minimal setup with auto-discovered allow_nets
  • examples/server-dns-override.yaml — adds per-profile DNS override
  • examples/server-multi-profiles.yaml — multiple profiles with different scopes
  • examples/server.json — JSON variant (also valid since JSON ⊂ YAML)

Docs updated: top of docs/usage.rst now links to these examples and reminds where to place the file on the server (/etc/sshuttle/server.yaml or ~/.config/sshuttle/server.yaml).

If you want additional scenarios (e.g., audit-only logs, UDP-only, or deny-by-default with explicit allows), I can add more examples.

…link from usage; tests: add negative no-config case to scripts/test-profiles
…eset behavior; logging to syslog; tests: add blocked-port check to test-profiles
@brianmay
Copy link
Member

Did you mean to include the .venv directory?

@sandinak
Copy link
Author

sandinak commented Oct 7, 2025

Update: TCP reset + syslog logging + username verification

Changes implemented:

  1. TCP reset for blocked connections

    • Added CMD_TCP_RESET (0x420f) to the mux protocol
    • Server sends reset when TCP connections are blocked by profile rules
    • Client immediately closes local socket on reset, providing fast failure (RST-like behavior)
  2. Syslog integration

    • Profiled mode: logs to configured log_path AND emits to syslog via OS logger utility
    • Unprofiled mode: emits syslog-style entries via OS logger utility
    • Format: action=ALLOWED/BLOCKED proto=TCP user=<ssh-user> profile=<name> src=<ip> spt=<port> dst=<ip> dpt=<port>
  3. Username logging

    • Server-side logs include user=<ssh-login> field in both profiled and unprofiled modes
    • Uses _current_user() to get the effective SSH login username
  4. Documentation updates

    • Updated manpage with TCP reset behavior and syslog logging details
    • Added note that --profile implies --auto-nets when no subnets specified

E2E test results:

Allowed traffic: curl to nginx:8080 works, logged with action=ALLOWED user=test
Blocked traffic: connection to port 4444 fails quickly (RST-like), logged with action=BLOCKED
Username verification: user=test appears in server logs as expected
Negative test: --profile without server config fails with clear error message
Lint/tests: flake8 clean, pytest 73 passed

Key log entries from E2E run:

Sep 19 19:18:28 sshuttle[359]: action=ALLOWED proto=TCP user=test profile=testing src=10.26.161.106 spt=51484 dst=10.55.2.77 dpt=8080

The implementation provides operational visibility through syslog while maintaining fast failure behavior for blocked TCP connections.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants