wijjit_ssh.config

Server configuration: every knob WijjitSSH takes.

One dataclass rather than a long keyword list, so a deployment can build its config once (from a file, from the environment, from argparse), inspect it, pass it around, and diff it against the defaults.

Every limit ships with a real default. The reasoning is that a server whose limits are opt-in is a server that is unbounded in every deployment where nobody thought about it, which is most of them. The defaults here (100 sessions, 10 per IP, a 10-minute idle timeout) are meant to be invisible to a legitimate user and finite to an abusive one.

The one deliberately-off default is connect_rate: rate limiting a service you have not measured is how you throttle your own health check, so it is opt-in.

Examples

Defaults, with just the two things that have no sensible default:

>>> config = ServerConfig(
...     host_keys=[ensure_host_key("ssh_host_key")],
...     auth=AuthorizedKeys("~/.ssh/authorized_keys"),
... )

A tighter posture for an exposed deployment:

>>> config = ServerConfig(
...     port=22,
...     host_keys=load_host_keys(["/var/lib/myapp/host_key"]),
...     auth=AuthorizedKeys("/etc/myapp/authorized_keys"),
...     max_sessions=25,
...     max_per_ip=2,
...     connect_rate=1.0,
...     idle_timeout=120.0,
... )
class wijjit_ssh.config.ServerConfig(host='', port=8022, host_keys=(), auth=None, allow_anonymous=False, max_sessions=100, max_per_ip=10, connect_rate=0.0, connect_burst=20, login_timeout=30.0, idle_timeout=600.0, session_timeout=None, keepalive_interval=30.0, keepalive_count_max=3, shutdown_grace=5.0, banner=None, on_event=None)[source]

Bases: object

Everything WijjitSSH needs to serve.

Parameters:
  • host (str) – Bind address. Default "" (all interfaces). Use "127.0.0.1" to serve only local clients.

  • port (int) – Bind port. Default 8022 - a high port, so the server needs no privileges. Pass 0 to let the OS choose (tests do this).

  • host_keys (sequence of str, PathLike, or asyncssh.SSHKey) – The server’s identity; see wijjit_ssh.keys. Paths are resolved when the server is constructed. Passing several supports rotation. Required to serve, though not to construct.

  • auth (AuthPolicy, optional) – How clients authenticate; see wijjit_ssh.auth. Required unless allow_anonymous=True.

  • allow_anonymous (bool) – Permit running with no authentication. Default False. This exists so that serving an unauthenticated SSH server is something you type, not something you inherit by forgetting an argument.

  • max_sessions (int) – Concurrent sessions across the whole server; further sessions are refused with a message. Default 100. Each session is a live Wijjit app, so this is really a memory bound - size it against what your app costs.

  • max_per_ip (int) – Concurrent connections from one IP, refused pre-authentication. Default 10. Note this counts connections rather than sessions: the point is to reject an abusive peer before spending a key exchange on it, and at that moment no session exists yet. Sessions per IP are transitively bounded by this, since every session lives inside a connection.

  • connect_rate (float) – Sustained connections per second per IP, as a token bucket refill rate. Default 0.0, which disables rate limiting: throttling a service you have not measured mostly succeeds at throttling your own health check.

  • connect_burst (int) – Bucket capacity for connect_rate - how many connections an IP may make at once before the sustained rate binds. Default 20. Ignored when connect_rate is 0.

  • login_timeout (float) – Seconds a client may take to authenticate before being dropped. Default 30.0, tightening asyncssh’s own 120s. Bounds how long an unauthenticated peer can hold resources.

  • idle_timeout (float or None) – Seconds without client input before a session is closed, or None to disable. Default 600.0. This is what reaps the forgotten ssh window that would otherwise hold a session slot forever.

  • session_timeout (float or None) – Hard cap on session duration regardless of activity, or None to disable. Default None. Unlike idle_timeout this will interrupt someone who is actively using the app, so it is off unless a deployment wants it.

  • keepalive_interval (float) – Seconds between keepalives on an idle connection, or 0 to disable. Default 30.0. Reaps peers whose TCP connection died without a FIN (a laptop lid, a NAT timeout) and would otherwise linger.

  • keepalive_count_max (int) – Unanswered keepalives before the connection is dropped. Default 3, so a dead peer is reclaimed in ~90s at the default interval.

  • shutdown_grace (float) – Seconds stop() gives sessions to exit cleanly before cancelling them. Default 5.0. Cleanly means the app’s teardown runs and the client’s terminal is restored; cancelling skips that, so this trades shutdown latency against leaving a client’s terminal in the alternate screen buffer.

  • banner (str or None) – Text sent to clients before authentication, or None. Default None. Shown by the client even if auth then fails, so it is the place for a legal notice, not for anything you would rather an unauthenticated stranger did not read.

  • on_event (callable, optional) – (event: str, fields: Mapping[str, object]) -> None, called for lifecycle events; see EventEmitter. Lets a deployment wire up metrics without this package depending on a metrics library. Exceptions from the hook are logged and swallowed.

Raises:

ValueError – If any value is out of range.

Examples

>>> config = ServerConfig(max_sessions=10, idle_timeout=60.0)
>>> config.max_sessions
10
>>> ServerConfig(max_sessions=0)
Traceback (most recent call last):
    ...
ValueError: max_sessions must be >= 1, got 0
replace(**overrides)[source]

Return a copy with overrides applied, re-validated.

Unlike dataclasses.replace(), unknown names raise rather than being silently accepted. That matters for a limits API: a typo’d max_session=1 that quietly does nothing leaves a server the operator believes is bounded and which is not.

Parameters:

**overrides (Any) – Field names and values to change.

Returns:

A new, validated config. The original is unchanged.

Return type:

ServerConfig

Raises:

Examples

>>> base = ServerConfig(max_sessions=100)
>>> base.replace(max_sessions=5).max_sessions
5
>>> base.max_sessions   # unchanged
100