[{"data":1,"prerenderedAt":314},["ShallowReactive",2],{"doc-\u002Fsecurity\u002Farchitecture":3},{"id":4,"title":5,"body":6,"description":304,"edit":305,"extension":306,"meta":307,"navigation":308,"path":309,"seo":310,"stem":311,"vertical":305,"weight":312,"__hash__":313},"content\u002Fsecurity\u002Farchitecture.md","Security architecture",{"type":7,"value":8,"toc":291},"minimark",[9,13,18,21,25,69,79,83,103,107,198,201,205,223,227,246,253,256,288],[10,11,12],"p",{},"This page describes the production architecture of OpenSense as it\nexists today (2026-05). It is opinionated and pragmatic, not a Tier-I\ndata centre. We optimise for \"small business operator can answer their\ninsurance questionnaire honestly\" rather than \"ISO 27001 today\".",[14,15,17],"h2",{"id":16},"data-flow","Data flow",[19,20],"arch-diagram",{},[14,22,24],{"id":23},"where-the-data-lives","Where the data lives",[26,27,28,41,51,57,63],"ul",{},[29,30,31,35,36,40],"li",{},[32,33,34],"strong",{},"Region",": ",[37,38,39],"code",{},"eu-central"," (Hetzner Falkenstein, Germany).",[29,42,43,46,47,50],{},[32,44,45],{},"Host",": a dedicated Hetzner VPS (",[37,48,49],{},"pingerHE","), one EU subnet, EU\nlegal entity for the data controller relationship.",[29,52,53,56],{},[32,54,55],{},"Database",": TimescaleDB on the same host, on a dedicated volume.",[29,58,59,62],{},[32,60,61],{},"Object storage"," (for PDFs and backups): a separate Hetzner volume,\nin the same DC. Encrypted at rest via LUKS.",[29,64,65,68],{},[32,66,67],{},"MQTT broker",": EMQX, single instance, same host.",[10,70,71,72,75,76,78],{},"There is ",[32,73,74],{},"no"," US-region failover. There is ",[32,77,74],{}," off-EU replica.",[14,80,82],{"id":81},"encryption","Encryption",[26,84,85,91,97],{},[29,86,87,90],{},[32,88,89],{},"In transit, public",": TLS 1.3 only, via Caddy. Certificates from\nLet's Encrypt, auto-renewed. SSL Labs grade A+.",[29,92,93,96],{},[32,94,95],{},"In transit, internal",": localhost loop on the host. Postgres and\nEMQX are bound to 127.0.0.1. There is no encryption on those loops\nbecause they do not leave the host.",[29,98,99,102],{},[32,100,101],{},"At rest",": LUKS on the data volume. Backups are encrypted with\nage (the keypair lives outside the host).",[14,104,106],{"id":105},"retention","Retention",[108,109,110,123],"table",{},[111,112,113],"thead",{},[114,115,116,120],"tr",{},[117,118,119],"th",{},"Data class",[117,121,122],{},"Default retention",[124,125,126,135,143,150,158,166,174,182,190],"tbody",{},[114,127,128,132],{},[129,130,131],"td",{},"Raw measurements",[129,133,134],{},"13 months",[114,136,137,140],{},[129,138,139],{},"5-minute rollups",[129,141,142],{},"5 years",[114,144,145,148],{},[129,146,147],{},"1-hour rollups",[129,149,142],{},[114,151,152,155],{},[129,153,154],{},"1-day rollups",[129,156,157],{},"forever (small)",[114,159,160,163],{},[129,161,162],{},"Alert events",[129,164,165],{},"forever",[114,167,168,171],{},[129,169,170],{},"PDF reports rendered",[129,172,173],{},"regenerated on demand; not stored long-term",[114,175,176,179],{},[129,177,178],{},"API request logs",[129,180,181],{},"30 days",[114,183,184,187],{},[129,185,186],{},"Outbound notification logs",[129,188,189],{},"90 days",[114,191,192,195],{},[129,193,194],{},"Backups (PG dump + storage)",[129,196,197],{},"30 days, hourly retained for 7 d, daily for 30",[10,199,200],{},"Retention is non-negotiable downward. A customer asking us to drop raw\nretention to 30 d is asking for a non-compliant product; we refuse.\nUpward extensions are a Team-tier or Enterprise-tier option.",[14,202,204],{"id":203},"backups","Backups",[26,206,207,210,213,216],{},[29,208,209],{},"Hourly logical Postgres dumps to the dedicated backup volume, encrypted\nwith age, retained 7 days.",[29,211,212],{},"Daily full dumps retained 30 days.",[29,214,215],{},"Weekly off-site dump to a separate Hetzner storage box, also encrypted.",[29,217,218,219,222],{},"We have ",[32,220,221],{},"not"," tested restore from offsite in the last 30 days.\nRestoring it monthly is a Q3 task; until then, treat the off-site dump\nas belt-and-braces, not as guaranteed.",[14,224,226],{"id":225},"secrets","Secrets",[26,228,229,232,235],{},[29,230,231],{},"Per-device ingest tokens — bcrypt-stored, last-used timestamped,\nrotatable any time.",[29,233,234],{},"User API tokens — same.",[29,236,237,238,241,242,245],{},"Postgres \u002F EMQX \u002F Postmark \u002F Telegram bot secrets — held in a single\nenv file owned by ",[37,239,240],{},"root:root",", mode ",[37,243,244],{},"0400",". No HashiCorp Vault, no\nKMS. Single-tenant infrastructure does not justify the operational\noverhead.",[14,247,249,250,252],{"id":248},"what-is-not-in-this-architecture","What is ",[32,251,221],{}," in this architecture",[10,254,255],{},"We are honest about the gaps so customers do not assume more than is\nthere.",[26,257,258,264,270,276,282],{},[29,259,260,263],{},[32,261,262],{},"No HSM."," All keys are in process memory.",[29,265,266,269],{},[32,267,268],{},"No SOC 2 \u002F ISO 27001 attestation."," Roadmap, not today.",[29,271,272,275],{},[32,273,274],{},"No SAML \u002F SSO."," Magic-link only. Coming with the Team tier.",[29,277,278,281],{},[32,279,280],{},"No air-gapped on-prem offering."," Not on the roadmap.",[29,283,284,287],{},[32,285,286],{},"No live failover."," A 30-minute downtime budget is acceptable to\nthe customer base (smaller than a fridge defrost cycle).",[10,289,290],{},"For customers who need any of the above, talk to us — we will tell you\nhonestly whether OpenSense fits.",{"title":292,"searchDepth":293,"depth":293,"links":294},"",3,[295,297,298,299,300,301,302],{"id":16,"depth":296,"text":17},2,{"id":23,"depth":296,"text":24},{"id":81,"depth":296,"text":82},{"id":105,"depth":296,"text":106},{"id":203,"depth":296,"text":204},{"id":225,"depth":296,"text":226},{"id":248,"depth":296,"text":303},"What is not in this architecture","Ingest, storage, encryption, retention",null,"md",{},true,"\u002Fsecurity\u002Farchitecture",{"title":5,"description":304},"security\u002Farchitecture",510,"oIVkBNDllWVEZjZLXl-Yv3TKSM5doycH8AXZPZctmVI",1779022955920]