gatekit

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: MIT

README

gatekit

Shared node-side library for fingerprint gates.

A gate is a small TCP proxy that passively fingerprints a client's plaintext handshake before forwarding it to the real backend, and uses that fingerprint as a noise filter: known-good clients pass, unknown ones are recorded for review, blocked ones are dropped. sshgate does this for the SSH KEXINIT (HASSH-style); tlsgate does it for the TLS ClientHello (JA3/JA4).

Both had independently grown the same ~1,500 lines: a SQLite fingerprint store, a gatehub sync client, a per-IP token bucket, and a concurrency cap. gatekit is that spine, factored out once.

It is not a control plane — that's gatehub. gatekit is what runs on the node and talks to it.

Packages

Package What it provides
store SQLite fingerprint store: entries, verdicts, labels, per-fingerprint IP/port sighting lists, sighting counts, pruning, and a protocol-agnostic metadata bag
controlplane gatehub sync client — pushes observations, pulls and applies policy
ratelimit Per-source-IP token bucket, IPv6-masked to /64, with a bounded bucket map
semaphore Global concurrency cap for in-flight connections

Protocol metadata

The store deliberately does not know about SSH or TLS. Each entry carries a Meta map[string]any that the gate fills with whatever its fingerprinter produced — kex/cipher_c2s/host_key for SSH, sni/alpn/ja4 for TLS — persisted as one JSON column. The same bag rides through to gatehub as the observation's metadata field, which is why one sync client can serve every gate and a new gate needs no schema change.

st, err := store.Open(store.Options{Path: "/var/lib/mygate/db.sqlite"})
if err != nil {
    return err
}
defer st.Close()

entry, err := st.Observe(store.Observation{
    Fingerprint: fp.Hash,
    IP:          clientIP,
    Port:        localPort,
    Meta:        map[string]any{"sni": fp.SNI, "alpn": fp.ALPN},
}, allowUnknown == false)
if err != nil {
    return err
}
if entry.Status == store.StatusBlocked {
    return errBlocked
}

Observe inserts on first sight and, on every later sight, refreshes only last_seen, the sighting count, and the metadata bag. Status, label, and first_seen are left alone, so an operator's verdict survives re-observation — and a verdict written ahead of time by UpsertStatus (how gatehub pre-approves a fingerprint on a fresh node) survives the client's first real connection.

Migrating an existing gate database

sshgate and tlsgate both have databases in service, with protocol fields in dedicated typed columns. Pass a Legacy mapping and Open folds those values into the metadata bag exactly once:

st, err := store.Open(store.Options{
    Path: "/var/lib/tlsgate/db.sqlite",
    Legacy: []store.LegacyColumn{
        {Column: "ja3", MetaKey: "ja3"},
        {Column: "sni", MetaKey: "sni"},
        {Column: "alpn", MetaKey: "alpn", Kind: store.KindJSON},
    },
})

The migration is designed to be boring in the ways that matter:

  • Approvals, blocks, labels, first-seen dates and sighting lists are preserved. They live in columns gatekit already understands.
  • It runs once, guarded by a marker in the meta table, so metadata the gate has refreshed since is never reverted on a restart.
  • It never clobbers a key already present in the bag.
  • The legacy columns are left in place. They all carry defaults, so gatekit's inserts ignore them — which means rolling back to the pre-gatekit binary finds its schema intact. Rollback is a binary swap, not a restore.

store/legacy_test.go exercises this against verbatim copies of both gates' production schemas.

Status

v0.1 — store, control plane, rate limiter, semaphore.

Not yet extracted: the proxy accept/peek/splice loop (and with it the Fingerprinter interface), the list/approve/block/pending/label/ delete/correlate CLI verbs, and the shared Ansible role. Those land in v0.2, once the store API has been proven against both gates in production.

License

MIT

Directories

Path Synopsis
Package controlplane implements the gate side of the gatehub sync protocol: it pushes observed fingerprints upstream and applies the verdicts gatehub sends back.
Package controlplane implements the gate side of the gatehub sync protocol: it pushes observed fingerprints upstream and applies the verdicts gatehub sends back.
Package ratelimit provides the per-source-IP token bucket a gate uses to cap how fast a single client can have connections processed.
Package ratelimit provides the per-source-IP token bucket a gate uses to cap how fast a single client can have connections processed.
Package semaphore provides a counting semaphore for bounding the number of connections a gate processes concurrently.
Package semaphore provides a counting semaphore for bounding the number of connections a gate processes concurrently.
Package store is the SQLite fingerprint store shared by gatekit gates.
Package store is the SQLite fingerprint store shared by gatekit gates.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL