diff --git a/README.md b/README.md index 1e206a1..57158d2 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ Bitcoin and self-hosting infrastructure runs on the machine. |---|---|---| | **Desktop** | Everyday users and computers with modest hardware | Sparrow, Bisq, and Bisq 2 for self-custody and peer-to-peer Bitcoin use | | **Node** | People ready to verify and operate their own Bitcoin infrastructure | Everything in Desktop, plus the full Bitcoin stack: Bitcoin Core, Electrs, LND, Ride The Lightning, BTCPay Server, and wallet-to-node connections | -| **Server + Desktop** | Bitcoiners who want the same sovereignty over their communications, cloud, passwords, and web services | The complete Node stack, plus the private self-hosted services | +| **Server + Desktop** | Bitcoiners who want the same sovereignty over their communications, cloud, passwords, and web services | The complete Node stack, plus the private self-hosted services. **Makes your home IP address public:** [read this first](#server--desktop-and-your-home-ip-address) | **Desktop: start with your keys.** Desktop is not a reduced or Bitcoin-free edition. It is a complete, private everyday computer with a clean GNOME @@ -237,6 +237,66 @@ communications, identity, and services. > provider allows port forwarding. Most home routers and providers already > support this. If you are unsure, a quick search for your router model and > "port forwarding" will usually turn up a step-by-step guide. +> +> **This mode also makes your home IP address public.** Read +> [what that means](#server--desktop-and-your-home-ip-address) before you +> choose it. + +### Server + Desktop and your home IP address + +> **⚠️ Server + Desktop makes your home IP address public.** +> Public services need a domain name that points at your home internet +> connection. When you finish the guided domain setup, Sovran_SystemsOS puts +> your home's public IP address in a Dynamic DNS record at +> [Njal.la](https://njal.la) and keeps it up to date, and you forward ports 80 +> and 443 on your router to this computer. From then on: +> +> - **Anyone can look up your domain and see your home IP address.** An IP +> address typically reveals your internet provider and your approximate +> location, and it ties everything you publish on that domain to your home +> connection. +> - **Domain privacy does not hide it.** Registrar privacy protects the +> registrant's identity, not the IP address in your DNS records. +> - **Your connection is open to the whole internet on those ports.** Scanners +> and bots constantly probe public IP addresses, so expect automated probing +> and login attempts against every service you publish. +> - **Your service names are discoverable.** Public HTTPS certificates are +> listed in public Certificate Transparency logs, so hostnames such as +> `vault.yourdomain.com` can be found, and then resolved to your IP address, +> even if you never share them. + +Nothing is published until you finish domain setup and port forwarding, but that +setup is the point of this mode, so assume your IP address will be public. +**Desktop** publishes nothing. **Node** publishes nothing unless you turn on a +feature that needs a domain: *Put BTCPay Server Online* or *Lightning Wallet +Connections*. + +**If you do not want your home IP address to be public,** choose Desktop or +Node. Advanced users can put a VPS, reverse proxy, or tunnel in front of their +services so DNS points there instead of at their home. Sovran_SystemsOS does not +set this up for you, and the Hub's domain checks currently expect DNS to point +at your home IP address. + +
+What happens technically + +- You create a **Dynamic** DNS record at Njal.la and paste its update command + into the Hub. The Hub only accepts `njal.la` update URLs. +- The `sovran-ddns-update` timer asks Njal.la to point your record at the + address the request came from. It does this right after you save a domain, + two minutes after boot, and then every 15 minutes. +- Njal.la reports that address back, and Sovran_SystemsOS keeps it for Element + calling and the Hub. Nothing else looks up your public IP address: no STUN + server, public DNS resolver, or "what is my IP" service is involved. See + `modules/core/njalla.nix`. +- Once a service that needs a domain is turned on, the firewall opens TCP and + UDP ports 80 and 443 for Caddy, which requests public HTTPS certificates for + the domains you configure. See `modules/core/caddy.nix`. +- Optional features can need more ports. Element calling, for example, needs + TCP 7881 and UDP 3478, 7882, and 40000–40099. The Hub lists the ports each + feature needs. + +
--- @@ -730,7 +790,9 @@ Sovran_SystemsOS uses layered controls: - Separate service users, systemd sandboxing, and loopback bindings where practical - Tor enforcement for supported Bitcoin services - Restricted, time-limited support access with scoped `sudo` -- Operator-controlled public service exposure +- Operator-controlled public service exposure (Server + Desktop + [makes your home IP address public](#server--desktop-and-your-home-ip-address) + once you set up a domain) See [`SECURITY.md`](SECURITY.md) for the threat model, limitations, reporting, and operator guidance. No operating system can protect funds after recovery diff --git a/SECURITY.md b/SECURITY.md index f95f02b..7ea8960 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Release | Supported | |---|:---:| -| Latest `1.0.x` stable release | Yes | +| Latest stable release | Yes | | `main` / `staging-dev` | Development only | | Older than `1.0.0` | No | @@ -35,6 +35,23 @@ external networks and are outside a “fully offline” model. The local Hub currently uses HTTP. Authentication does not encrypt local network traffic, so use a trusted LAN and avoid public or guest Wi-Fi. +### Public services and your home IP address + +Server + Desktop publishes services under your own domain. The Dynamic DNS +record at Njal.la then points at your home's public IP address, which anyone +can look up (domain privacy does not hide it), and ports 80 and 443 are open +to the whole internet. Public HTTPS certificates also list your service +hostnames in Certificate Transparency logs. Desktop publishes nothing. Node +publishes nothing unless *Put BTCPay Server Online* or *Lightning Wallet +Connections* is on. See +[Server + Desktop and your home IP address](README.md#server--desktop-and-your-home-ip-address) +for what this means and the alternatives. + +Sovran_SystemsOS does not ask a STUN server, public DNS resolver, or “what is +my IP” service for your address. The DDNS update asks Njal.la to use the +address the request came from, and the address Njal.la reports back is the one +Element calling and the Hub use. + ### Bitcoin stack Bitcoin and Lightning modules are maintained in the standalone @@ -64,7 +81,8 @@ change both the system and local configuration. - Separate service users and systemd sandboxing where supported - Administrative service ports bound to loopback where practical - Tor enforced for supported Bitcoin traffic and onion services -- Public web services exposed only when enabled by the operator +- Public web services exposed only when enabled by the operator (this makes + your home IP address public) Tor reduces network exposure for configured Bitcoin services. It is not a guarantee against every IP leak, application bug, or traffic-analysis attack. diff --git a/app/sovran_systemsos_web/static/js/domain-prereqs.js b/app/sovran_systemsos_web/static/js/domain-prereqs.js index a1e0261..a91b32f 100644 --- a/app/sovran_systemsos_web/static/js/domain-prereqs.js +++ b/app/sovran_systemsos_web/static/js/domain-prereqs.js @@ -43,6 +43,11 @@ function renderDomainNeedsHtml(opts) { + 'Njal.la' + " and connecting your services. Just follow the steps below.

"; } + // Every variant above ends with a domain that points at the user's home + // connection. Say what that publishes (README: "Server + Desktop and your + // home IP address"). + html += "

⚠️ Heads-up: your domain points at your home internet connection, " + + "so anyone can look up your home IP address. Domain privacy does not hide it.

"; return html; } diff --git a/app/sovran_systemsos_web/templates/index.html b/app/sovran_systemsos_web/templates/index.html index 4aa6e24..45468d5 100644 --- a/app/sovran_systemsos_web/templates/index.html +++ b/app/sovran_systemsos_web/templates/index.html @@ -476,6 +476,10 @@
  • To make your services available outside your home, complete one router task: forward ports 80 and 443 to this computer
  • +

    + ⚠️ Heads-up: your domain points at your home internet connection, + so anyone can look up your home IP address. Domain privacy does not hide it. +

    The Hub guides you through every step.

    diff --git a/iso/installer.py b/iso/installer.py index 173a6f8..adb5f79 100644 --- a/iso/installer.py +++ b/iso/installer.py @@ -471,7 +471,7 @@ class InstallerWindow(Adw.ApplicationWindow): # Role cards roles = [ ("Server + Desktop", - "Full sovereignty: host your own websites, cloud, chat, passwords, and Bitcoin services instead of relying on Big Tech. Sovran_SystemsOS walks you through getting your domain from Njal.la and connecting everything. One router task is required: forward ports 80 and 443 to this computer.", + "Full sovereignty: host your own websites, cloud, chat, passwords, and Bitcoin services instead of relying on Big Tech. Sovran_SystemsOS walks you through getting your domain from Njal.la and connecting everything. One router task is required: forward ports 80 and 443 to this computer. Heads-up: this makes your home IP address public.", "Server+Desktop"), ("Desktop Only", "A beautiful, easy-to-use desktop without the background server applications.", diff --git a/tests/test_exposure_guards.py b/tests/test_exposure_guards.py new file mode 100644 index 0000000..9aa4782 --- /dev/null +++ b/tests/test_exposure_guards.py @@ -0,0 +1,68 @@ +"""Guards for the home-IP warnings. + +Server + Desktop publishes the home IP address (the domain points at it), so every +place that offers Server + Desktop must say so, and the README section they point +at must exist. + +These read the shipped source files like the nix-file checks in test_security.py: +nothing is run and nothing touches the network. +""" + +import os +import re +import unittest + +_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..")) +_PHRASE = "home ip address" + + +def _read(*parts): + with open(os.path.join(_ROOT, *parts), encoding="utf-8") as f: + return f.read() + + +def _github_slug(heading): + slug = re.sub(r"[^\w\- ]", "", heading.strip().lower()) + return slug.replace(" ", "-") + + +class HomeIpWarnings(unittest.TestCase): + + def test_installer_role_card(self): + src = _read("iso", "installer.py") + card = re.search(r'\("Server \+ Desktop",\s*"((?:[^"\\]|\\.)*)"', src, re.S) + self.assertIsNotNone(card, "Server + Desktop role card not found") + self.assertIn(_PHRASE, card.group(1).lower()) + + def test_hub_domain_setup_text(self): + # domain-prereqs.js is the single source for onboarding, feature setup + # and domain reconfiguration; the notice must follow every variant. + js = _read("app", "sovran_systemsos_web", "static", "js", "domain-prereqs.js") + body = js[js.index("function renderDomainNeedsHtml"):] + body = body[:body.index("\n}\n")] + self.assertIn(_PHRASE, body.lower()) + self.assertGreater(body.lower().index(_PHRASE), body.rindex("} else {"), + "the notice must come after the last variant, not inside one") + + def test_hub_upgrade_dialog(self): + html = _read("app", "sovran_systemsos_web", "templates", "index.html") + dialog = html[html.index('id="upgrade-modal"'):html.index("Security Reset overlay")] + self.assertIn(_PHRASE, " ".join(dialog.lower().split())) + + def test_readme_and_security_policy(self): + self.assertIn(_PHRASE, _read("README.md").lower()) + self.assertIn(_PHRASE, " ".join(_read("SECURITY.md").lower().split())) + + def test_links_to_the_readme_section_resolve(self): + readme = _read("README.md") + slugs = {_github_slug(m.group(2)) + for m in re.finditer(r"^(#{1,6})\s+(.+?)\s*$", readme, re.M)} + links = re.findall(r"\]\(#(server--desktop[^)]*)\)", readme) + links += re.findall(r"README\.md#(server--desktop[^)\s]*)", _read("SECURITY.md")) + self.assertTrue(links, "expected links to the home-IP section") + for anchor in links: + self.assertIn(anchor, slugs) + + +if __name__ == "__main__": + unittest.main()