IronShep POC — technology inventory¶
Every version below was read from the resolved dependency graph
(cargo tree), the running toolchain, and the live database on
2026-08-23 — not from the version ranges in Cargo.toml. Those ranges
are semver-flexible, so a fresh cargo build on a different day may resolve
a newer patch. Where the two differ, the manifest range is shown alongside.
Repos: ironshep-core v0.2.0 · ironshep-edge v0.2.0 — both Rust
edition 2021.
Shipping artefact is two static binaries. Release builds (LTO + symbol
stripping) on aarch64-apple-darwin:
| Binary | Release size | Contains |
|---|---|---|
ironshep-core |
7.5 MB | gRPC ingest server, TimescaleDB writer, the web console, and the notification senders (SMTP + webhooks) |
ironshep-edge |
7.0 MB | six protocol listeners and the buffering forwarder |
No runtime, no interpreter, no node_modules, no container image required.
1. Language & toolchain¶
| Component | Version | Notes |
|---|---|---|
Rust (rustc) |
1.98.0 (88d9e12ae) | edition 2021 |
| Cargo | 1.98.0 (797e8a9bc) | |
| Host triple (dev) | aarch64-apple-darwin |
production target is x86_64-unknown-linux-gnu (RHEL 9) |
| Build deps (RHEL) | gcc, openssl-devel |
openssl-devel is only needed by the opcua crate on the edge |
No protoc anywhere. The gRPC contract is compiled by protox, a pure-Rust
protobuf compiler, so there is no external binary to install or version-match
on any host.
No Node, npm, or bundler. The web console is hand-written HTML/CSS/JS compiled into the core binary — see §5.
2. Shared foundation (both binaries)¶
| Crate | Resolved | Manifest | Role |
|---|---|---|---|
tokio |
1.53.1 | 1 (features full) |
async runtime |
tonic |
0.12.3 | 0.12 (feature tls) |
gRPC — server in core, client in edge |
prost |
0.13.5 | 0.13 |
protobuf codegen runtime |
serde |
1.0.229 | 1 (derive) |
serialization |
serde_json |
1.0.151 | 1 |
JSON — API responses, attributes columns |
toml |
0.8.23 | 0.8 |
config files |
chrono |
0.4.45 | 0.4 (serde) |
timestamps |
tracing |
0.1.44 | 0.1 |
structured logging |
tracing-subscriber |
0.3.23 | 0.3 (env-filter) |
RUST_LOG handling |
anyhow |
1.0.104 | 1 |
error handling |
Build-dependencies (both): tonic-build 0.12.3, protox 0.7.2.
Transport security stack (transitive)¶
| Crate | Resolved | Where |
|---|---|---|
rustls |
0.23.43 | gRPC/mTLS + Postgres TLS |
rustls |
0.22.4 | pulled in by an older-API dependency; both coexist |
tokio-rustls |
0.26.4 / 0.25.0 | matching the two rustls majors |
rustls-pemfile |
2.2.0 | certificate/key parsing |
ring |
0.17.14 | cryptographic primitives |
hyper |
1.11.0 | HTTP/2 transport under tonic and axum |
TLS is rustls, not OpenSSL — no system libssl dependency at runtime, which keeps the RHEL deployment insensitive to the host's OpenSSL version.
3. ironshep-core — ingest server + console¶
| Crate | Resolved | Manifest | Role |
|---|---|---|---|
sqlx |
0.8.6 | 0.8 |
Postgres driver — features postgres, runtime-tokio, tls-rustls, chrono, json; default-features = false |
axum |
0.7.9 | 0.7 |
web console HTTP server |
argon2 |
0.5.3 | 0.5 |
argon2id password hashing |
rand |
0.8.7 | 0.8 |
session token generation (OsRng) |
sha2 |
0.10.9 | 0.10 |
hashes session tokens at rest |
rpassword |
7.5.4 | 7 |
no-echo password prompt for --add-user |
reqwest |
0.12.28 | 0.12 |
Slack/Teams webhook delivery — rustls-tls, json; default-features = false |
lettre |
0.11.23 | 0.11 |
email (SMTP) delivery — smtp-transport, tokio1-rustls-tls, builder; default-features = false |
Both notification clients use rustls, so core still has no system OpenSSL dependency (matches the gRPC/DB TLS stack).
Dev-dependency: tokio-stream 0.1.19 — only for the mTLS integration test's
ephemeral-port listener.
Queries are runtime-checked, not compile-time macros, so building core
does not require a reachable database or a DATABASE_URL.
Unique crates in the graph, including transitive: 214.
4. ironshep-edge — protocol collector¶
| Crate | Resolved | Manifest | Role |
|---|---|---|---|
rumqttc |
0.24.0 | 0.24 |
MQTT subscriber |
tokio-modbus |
0.14.0 | 0.14 |
Modbus/TCP client — default-features = false, feature tcp |
opcua |
0.12.0 | 0.12 |
OPC-UA client — optional, feature-gated |
Feature flags: default = ["opcua-support"]. Build without OPC-UA via
cargo build --release --locked --no-default-features — that one crate carries the
largest dependency subtree and the least stable API.
Hand-written, zero-dependency decoders (deliberate — these are the layers you want to understand when a field device sends something strange):
| Protocol | Module | Coverage |
|---|---|---|
| SNMP / BER | src/listeners/snmp_ber.rs |
v1 + v2c traps and informs, plus v2c Get/GetNext encoding and typed response parsing for the poller; unit-tested |
| BACnet/IP | src/listeners/bacnet.rs |
BVLC/NPDU/APDU framing, full I-Am decode; unit-tested |
| Syslog | src/listeners/syslog.rs |
RFC 3164 + RFC 5424, UDP and TCP |
Unique crates in the graph, including transitive: 176.
Protocols carried today¶
| Protocol | Direction | Default port |
|---|---|---|
| Syslog | inbound UDP + TCP | 5514 |
| SNMP traps (v1/v2c) | inbound UDP | 5162 |
| SNMP polling (v2c) | outbound poll | 161 |
| BACnet/IP | inbound UDP | 47808 |
| MQTT | outbound to broker | 1883 |
| Modbus/TCP | outbound poll | 502 (1502 for the simulator) |
| OPC-UA | outbound poll | 4840 |
| gRPC → core | outbound, mTLS | 50051 |
SNMP is entirely the hand-written BER codec — the poller added the encode
direction to the same zero-dependency module. New listener:
src/listeners/snmp_poll.rs (scalar Gets, GetNext table walks, outage
transition events). SNMPv3 (USM) remains out of scope for both directions.
The other protocols on the marketing site (DNP3, EtherNet/IP, PROFINET,
IEC 61850, S7, HART, CoAP, LoRaWAN, NetFlow) are roadmap, not implemented —
see ironshep-site/CLAUDE.md.
5. Web console (inside ironshep-core)¶
| Layer | Technology | Version / size |
|---|---|---|
| Server | axum |
0.7.9 |
| Markup | hand-written HTML5 | app.html 3.6 KB · login.html 5.5 KB |
| Styles | hand-written CSS, custom properties | style.css 25.9 KB |
| Behaviour | vanilla ES2020+, hash routing | app.js 44.8 KB |
| Charts | inline SVG, no library | part of app.js |
| Login background | Canvas 2D particle network | part of login.html |
| Icons | inline SVG + brand-kit SVG/ICO | ~2.6 KB total |
Layout is a fixed left icon rail (Fleet · Signals · Events · Settings) beside a content column; entity detail opens in a right slide-in drawer, and the sign-in screen carries the ironshep.com particle-network canvas. Collapses to a bottom bar on phones. Settings holds an admin-only notification panel (email/Slack/Teams channels + triggers), gated on the session role.
Zero JavaScript dependencies. Zero external requests. No React, no
charting library, no web fonts, no CDN. Every asset is include_str!/
include_bytes!'d into the binary at compile time, which is what lets the
console work on an air-gapped network — a hard requirement for the OT
buyers this targets. Total UI payload: ~83 KB.
Typography uses the brand stack (Bookman Old Style → Cambria → Georgia serif; Calibri body; Courier New mono) resolved from system fonts, so nothing is fetched at runtime.
Browser APIs relied on: fetch, URLSearchParams, Canvas 2D (login
background), localStorage (motion preference), pointer events, CSS custom
properties, backdrop-filter. Targets current evergreen browsers; no
transpilation or polyfills.
6. Data layer¶
| Component | Version | Notes |
|---|---|---|
| PostgreSQL | 16.14 | production install via PGDG on RHEL 9 |
| TimescaleDB | 2.29.2 | hypertables + compression policies on events, telemetry |
timescaledb_toolkit |
1.25.0 | ships with the container; useful for rolling baselines in ironshep-ml |
pgvector (vector) |
0.8.6 | vector(768) column on log_templates, reserved for Phase-2 LLM work |
plpgsql |
1.0 | stock |
Local dev image: timescale/timescaledb-ha:pg16
(digest sha256:07122702…44d1a49), published on host port 5433.
Tables: events, telemetry (hypertables) · assets, anomalies,
log_templates, users, ui_sessions (regular).
7. Cryptography & auth¶
| Concern | Choice | Detail |
|---|---|---|
| Edge ↔ core transport | mTLS, mandatory | no plaintext mode exists |
| Certificates | ECDSA P-256, SHA-256 | 825-day leaves, single POC CA |
| Cert generation | openssl CLI |
scripts/gen_certs.sh |
| Console passwords | argon2id | argon2 0.5.3, PHC-string format |
| Console sessions | 256-bit random token | OsRng; only SHA-256 stored server-side |
| Cookie | HttpOnly, SameSite=Lax | 12h default TTL |
| Roles | admin / user |
notification setup is admin-only, enforced by an axum extractor (403 before body parse) |
| Notification secrets | plaintext at rest | SMTP password + webhook URLs in the DB; never returned to the browser (redacted to a *_set flag) — encrypting at rest is on the roadmap |
| Notification transport | rustls | webhooks over HTTPS; SMTP via STARTTLS/implicit TLS |
gen_certs.sh explicitly passes -sha256 and
-pkeyopt ec_param_enc:named_curve because LibreSSL on macOS defaults to
SHA-1 signatures and explicit EC parameters, both of which rustls rejects.
RHEL's OpenSSL 3 does the right thing by default; the flags are harmless there
and essential on a Mac.
Console TLS is not implemented — port 8080 is plain HTTP in the POC. Front it with a reverse proxy or keep it on a management network. This is item 2 on the roadmap.
8. Test & simulation tooling¶
| Tool | Version | Purpose |
|---|---|---|
cargo test |
built-in | 12 edge unit tests, 9 core unit tests, 2 core mTLS integration tests |
openssl |
LibreSSL 3.3.6 (macOS dev) | throwaway certs inside the integration test |
| Python | 3.9.6 | simulators only, never in the runtime path |
pymodbus |
3.8.6 | Modbus pump simulator with modelled degradation |
paho-mqtt |
not installed here | required only by mqtt_sim.py |
| Docker | 29.2.1 | local TimescaleDB only; nothing is containerised in production |
| Bash | 3.2.57 (macOS) / 5.x (RHEL) | scripts are written to the 3.2 floor for portability |
Simulator Python imports are all stdlib except pymodbus and paho.mqtt:
asyncio, json, math, random, socket, struct, sys, time.
9. Deployment & operations¶
| Component | Technology |
|---|---|
| Target OS | RHEL 9 (three hosts: database, core, edge) |
| Process supervision | systemd, hardened units (ProtectSystem=strict, NoNewPrivileges, CAP_NET_BIND_SERVICE on edge only) |
| Firewall | firewalld, scoped rich rules per source IP |
| SELinux | stays Enforcing — no custom policy required |
| Scheduled jobs | cron (crond) — synthetic traffic + health check on the edge |
| Log rotation | logrotate |
| Source hosting | GitLab |
| Web presence | GitLab Pages (ironshep-site, separate repo) |
10. Deliberate non-choices¶
Worth recording, because each was considered and rejected:
| Not used | Why |
|---|---|
protoc binary |
protox compiles protobuf in pure Rust — one less host dependency to version-match |
| OpenSSL at runtime | rustls avoids coupling to the host's libssl |
| React / Vue / any JS framework | ~83 KB of hand-written UI beats a build pipeline for this handful of views, and keeps the air-gap story true |
| Any charting library | inline SVG is ~200 lines and adds no supply-chain surface |
| Web fonts | system-font stack; nothing fetched at runtime |
| Docker in production | plain systemd units on RHEL; containers are dev-only |
| sqlx compile-time macros | would require a live database to build |
| Kafka / NATS / a message bus | the edge's bounded buffer plus core's gRPC backpressure covers POC scale |
An API between core and ironshep-ml |
the anomalies table is the contract — see ironshep-core/docs/ML_CONTRACT.md |
11. Not yet built¶
Named in the architecture, no code today:
| Module | Purpose | Status |
|---|---|---|
ironshep-ml |
anomaly detection → anomalies table |
contract defined, console side complete and waiting |
ironshep-llm |
log-template mining + embeddings → log_templates |
schema column reserved (vector(768)) |
ironshep-security |
Phase-4 security module | brand mark assigned (Anomaly Waveform), nothing else |
Reproducing this inventory¶
source "$HOME/.cargo/env"
cd ironshep-core && cargo tree --depth 1 --edges normal
cd ../ironshep-edge && cargo tree --depth 1 --edges normal
rustc --version && cargo --version
docker exec ironshep-db psql -U ironshep -d ironshep \
-tAc "SELECT extname||' '||extversion FROM pg_extension ORDER BY 1;"
Cargo.lock is committed in both repos, so the versions in this document
are the versions a build produces anywhere — the RHEL hosts resolve exactly
what was tested here. Lockfile format version 4.
To reproduce a build exactly as tested, and fail loudly rather than silently upgrading if anything drifts:
Use --locked in CI and in the deployment runbooks. To take deliberate
upgrades: cargo update (whole graph) or cargo update -p <crate> (one
crate), then re-run the test suites and refresh the version tables above.