[{"data":1,"prerenderedAt":366},["ShallowReactive",2],{"doc-\u002Fsecurity\u002Fthreat-model":3},{"id":4,"title":5,"body":6,"description":356,"edit":357,"extension":358,"meta":359,"navigation":360,"path":361,"seo":362,"stem":363,"vertical":357,"weight":364,"__hash__":365},"content\u002Fsecurity\u002Fthreat-model.md","Threat model",{"type":7,"value":8,"toc":341},"minimark",[9,18,23,28,31,37,43,71,77,81,84,89,93,114,119,123,126,131,135,161,167,178,182,185,190,194,205,210,214,217,222,226,238,243,247,250,255,259,273,278,282,285,290,294,309,314,318,321],[10,11,12,13,17],"p",{},"A real threat model, not a marketing sheet. We list each adversary\nclass, what they can do, what we do about it, and where we deliberately\ndo ",[14,15,16],"strong",{},"not"," harden.",[19,20,22],"h2",{"id":21},"adversaries","Adversaries",[24,25,27],"h3",{"id":26},"a-curious-internet","A — Curious internet",[10,29,30],{},"Random scanners and bots that scrape every exposed HTTPS endpoint\nlooking for misconfigurations.",[10,32,33,36],{},[14,34,35],{},"Capabilities",": send arbitrary HTTP to our edge. No insider access,\nno targeted persistence.",[10,38,39,42],{},[14,40,41],{},"What we do",":",[44,45,46,50,58,65,68],"ul",{},[47,48,49],"li",{},"TLS 1.3 only; SSL Labs A+.",[47,51,52,53,57],{},"All routes require either a session cookie or a bearer token; no\nunauthenticated GET to ",[54,55,56],"code",{},"api.opensense.murzin.digital\u002Fv1\u002F*",".",[47,59,60,61,64],{},"Aggressive rate limit on ",[54,62,63],{},"\u002Fauth\u002F*"," (per-IP, 10 req \u002F min).",[47,66,67],{},"The dashboard does not leak account-existence (auth\u002Frequest always\nreturns 200).",[47,69,70],{},"All errors carry no stack traces; the body is the structured error\nenvelope.",[10,72,73,76],{},[14,74,75],{},"What we do not do",": bot-blocking by JA3 fingerprint or Cloudflare-\nstyle challenge pages. Caddy is in front of the API; we accept the\nextra load from idle bot traffic.",[24,78,80],{"id":79},"b-ex-employee-of-a-customer","B — Ex-employee of a customer",[10,82,83],{},"Someone who once had access to a customer account, now no longer\nshould.",[10,85,86,88],{},[14,87,35],{},": still knows the magic-link email; may know an old\nsession cookie; may have copies of past PDFs.",[10,90,91,42],{},[14,92,41],{},[44,94,95,98,105,108,111],{},[47,96,97],{},"Magic-link tokens expire after 10 min and are single-use.",[47,99,100,101,104],{},"Session cookies can be revoked centrally (",[54,102,103],{},"Account → Sessions → Sign out everywhere",").",[47,106,107],{},"API tokens can be rotated; old tokens fail immediately.",[47,109,110],{},"Account holder can change the login email.",[47,112,113],{},"Audit log shows every login and is exportable.",[10,115,116,118],{},[14,117,75],{},": SAML SSO with org-level provisioning. The\noperator is responsible for changing the login email when staff turn\nover. We will add this at the Team tier.",[24,120,122],{"id":121},"c-compromised-device-on-customer-lan","C — Compromised device on customer LAN",[10,124,125],{},"A device on the customer's LAN that has been firmware-tampered or\nreplaced.",[10,127,128,130],{},[14,129,35],{},": can send arbitrary payloads to the ingest URL with\nthe captured device token; can fabricate readings.",[10,132,133,42],{},[14,134,41],{},[44,136,137,152,155,158],{},[47,138,139,140,143,144,147,148,151],{},"The token is ",[14,141,142],{},"per device",". A leaked token for ",[54,145,146],{},"fridge01"," cannot\ningest as ",[54,149,150],{},"fridge02"," — the device id is bound to the token\nserver-side.",[47,153,154],{},"Sanity-range checks reject implausible values at ingest. A\ncompromised device cannot inject \"+9999 °C\" successfully.",[47,156,157],{},"Audit log records every measurement; a customer reviewing past\nhistory can spot a step-discontinuity that doesn't match the\nphysical world.",[47,159,160],{},"The customer can rotate the token any time; on rotation, the old\ntoken is immediately invalid.",[10,162,163,166],{},[14,164,165],{},"What we cannot do",": tell a fabricated-but-plausible reading apart\nfrom a real one. If the attacker reports +4.3 °C for a freezer set to\n−18 °C, the reading is rejected as out-of-range. If the attacker\nreports −18.3 °C when the real freezer is at +9 °C, OpenSense believes\nthe attacker. That is a physical-security problem, not a software one.",[10,168,169,170,173,174,177],{},"For high-stakes cases (pharmaceutical cold chain), the regulated\npractice is ",[14,171,172],{},"independent calibration probes"," plus ",[14,175,176],{},"physical seals\non the sensor enclosure",". We support that workflow; we do not\nhardware-attest sensors ourselves.",[24,179,181],{"id":180},"d-network-attacker-on-the-device-to-edge-path","D — Network attacker on the device-to-edge path",[10,183,184],{},"Someone in the path between the sensor and our edge.",[10,186,187,189],{},[14,188,35],{},": passive observation, active modification of\nunencrypted traffic.",[10,191,192,42],{},[14,193,41],{},[44,195,196,199,202],{},[47,197,198],{},"Refuse non-TLS connections on the API entirely (Caddy redirects 80\nto 443 then HSTS).",[47,200,201],{},"MQTT bridge is mTLS-only.",[47,203,204],{},"LoRaWAN uplinks are AES-128 encrypted on the radio layer (LoRaWAN\n1.0.3+).",[10,206,207,209],{},[14,208,165],{},": protect Shelly Gen1 over plain HTTP on a\nhostile LAN. Gen1 firmware does not do HTTPS. We accept Gen1 traffic\nfrom the LAN to our edge over HTTPS — the gen1 is on a LAN, our edge\nis on the internet; if your AP is hostile, you have bigger problems.",[24,211,213],{"id":212},"e-network-attacker-between-our-edge-and-the-database","E — Network attacker between our edge and the database",[10,215,216],{},"\"Insider on Hetzner network\" or similar.",[10,218,219,221],{},[14,220,35],{},": passive observation of bytes between our nodes.",[10,223,224,42],{},[14,225,41],{},[44,227,228,235],{},[47,229,230,231,234],{},"All inter-process communication is on ",[54,232,233],{},"127.0.0.1"," of the same\nHetzner VPS. There is no cross-host service mesh.",[47,236,237],{},"LUKS at rest on the data volume.",[10,239,240,242],{},[14,241,165],{},": defend against a Hetzner staff member with\nphysical KVM access if the machine is powered on. The LUKS key is\nloaded at boot; if an adversary gets KVM with the machine running,\nthe key is in RAM. This is a residual single-VPS risk; the customer\nbase accepts it.",[24,244,246],{"id":245},"f-our-own-engineer-making-a-mistake","F — Our own engineer making a mistake",[10,248,249],{},"The biggest realistic threat for any small-team SaaS.",[10,251,252,254],{},[14,253,35],{},": production access via SSH; can deploy anything.",[10,256,257,42],{},[14,258,41],{},[44,260,261,264,267,270],{},[47,262,263],{},"All deploys via a tagged release. Rollback is a one-command revert.",[47,265,266],{},"Postgres is backed up hourly to a separate volume and weekly to a\nseparate storage box.",[47,268,269],{},"Schema migrations are tested in a dev environment before being\napplied to production.",[47,271,272],{},"Audit log is hash-chained — even with prod DB access, silent\nhistory rewrites invalidate the daily-published head.",[10,274,275,277],{},[14,276,165],{},": prevent every footgun from a sleep-deprived\nsolo founder. We minimise blast radius (small, focused services,\nimmutable measurement storage), but a \"DROP TABLE measurements\" by\ntypo is recoverable only from backup, with a measurement gap.",[24,279,281],{"id":280},"g-legal-govt-order","G — Legal \u002F Govt order",[10,283,284],{},"EU jurisdiction, lawful order to disclose customer data.",[10,286,287,289],{},[14,288,35],{},": a court order delivered to Murzin Digital s.r.o.",[10,291,292,42],{},[14,293,41],{},[44,295,296,299,302],{},[47,297,298],{},"We are an EU entity in Slovakia. We will comply with valid EU\njurisdiction orders. We will not voluntarily disclose to non-EU\nauthorities.",[47,300,301],{},"We disclose only what the order requires; we push back on overly\nbroad orders via counsel.",[47,303,304,305,308],{},"We will notify the affected customer ",[14,306,307],{},"unless"," the order forbids\nnotification.",[10,310,311,313],{},[14,312,75],{},": claim \"we cannot decrypt your data\" — that is\nnot true. Customer data is at-rest encrypted at the disk layer but\nthe application has plaintext access by design (we need it to render\ncharts and PDFs).",[19,315,317],{"id":316},"out-of-scope","Out of scope",[10,319,320],{},"We are explicit about the threats we do not address:",[44,322,323,329,335],{},[47,324,325,328],{},[14,326,327],{},"Side-channel attacks"," on the sensor (laser-microphone, RF\nemanations). Not a small-business risk model.",[47,330,331,334],{},[14,332,333],{},"Long-haul cryptographic resistance",". We do not signed-store\nmeasurements with a quantum-safe scheme. Audit hashes are SHA-256;\nif SHA-256 falls, the audit chain is rebuilt under a stronger\nprimitive at that time.",[47,336,337,340],{},[14,338,339],{},"Customer-side device firmware integrity."," We do not run a\nfirmware-signing program for Shelly \u002F Aqara devices; the vendors do.",{"title":342,"searchDepth":343,"depth":343,"links":344},"",3,[345,355],{"id":21,"depth":346,"text":22,"children":347},2,[348,349,350,351,352,353,354],{"id":26,"depth":343,"text":27},{"id":79,"depth":343,"text":80},{"id":121,"depth":343,"text":122},{"id":180,"depth":343,"text":181},{"id":212,"depth":343,"text":213},{"id":245,"depth":343,"text":246},{"id":280,"depth":343,"text":281},{"id":316,"depth":346,"text":317},"What we defend, what we don't",null,"md",{},true,"\u002Fsecurity\u002Fthreat-model",{"title":5,"description":356},"security\u002Fthreat-model",540,"ZDs50If9VJHr0uqbX2qdEbxOzJh3HKdX0uNBBLXHvZI",1779022956112]