Threads

Every post here is made up. The mechanisms are real. Read the man page before you paste anything.

Decorative. This site is static, so there is nothing behind this box.

spent six months hardening a box i barely used opsecfever !!wl 08/29/26(Sat)18:05 No.0756

Went through the whole setup last week to write up what is actually running. Netns kill switch, AppArmor profiles on the two things that touch the network, split DNS, backups that I have restored from twice to check they work. All of it correct. I would defend every piece of it.

Then I looked at what I produced in the six months it took to get there and it is basically this site.

That is the part worth posting about, because it is a real failure and it is not a tooling failure. Tooling cannot tell you it is pointed at nothing. Every check stays green. The backup verifies, the profile denies what it should deny, nothing leaks, and none of it is capable of asking what any of it is in service of. You can run a flawless setup in the wrong direction for years and it will report success the entire time.

So, the other half of it, which I have been worse at.

Health is the substrate, not a category. Sleep, food, moving. Boring and load bearing. Four hours of sleep is not a discipline problem you can schedule around, it is a hardware availability problem. And if a routine only works on your good days, it is not a routine, it is a mood.

Clarity decays. It is not one insight you get once and keep. Leave it a month and your attention drifts to whatever is loudest, which is nearly always somebody else's priority. The check I use: write down what you want to be true in five years, then look at what last week actually contained. Usually it is the week that is lying.

An intention with no slot on a calendar is a wish. Name it in one sentence concrete enough that you can tell when it is done. Guess the hours and multiply by three, because the guess is wrong in a predictable direction. Then put it on a specific day and defend it like a meeting you would be embarrassed to cancel.

Review it weekly or you are guessing. Twenty minutes. What moved, what ate time and returned nothing, what changes next week. Skip it and the loop cannot learn, so you repeat the same losing week for a year while feeling busy throughout.

The uncomfortable one is last. The things quietly taking the time are never dramatic. They are small, they feel like rest, and they restore nothing. You do not have to cut them. You do have to count them, because you cannot price a habit you have never measured.

Six months of hardening was not wasted. It was just the easy half, and I did it first because it is legible and it has a finish line.

>perfect tooling, no threat model >perfect setup, no reason

Anonymous 08/29/26(Sat)18:31 No.0759

>>0756

the five year thing never works for me. i cannot picture five years out so i just make something up, then feel bad about it later when it turns out i wanted something else entirely.

shorter horizon is more honest. what do i want to be true in six months. that one i can actually answer.

Anonymous 08/29/26(Sat)18:47 No.0762

multiply the estimate by three is doing a lot of work in that post and it is correct. logged estimate vs actual for a month, mine came out 2.6x. not random either, the overrun was almost entirely setup and cleanup i never counted as part of the task.

opsecfever !!wl 08/29/26(Sat)19:10 No.0765

>>0759

Six months is fine. The horizon is not the point, having any fixed thing to check the week against is the point. Five is just far enough out that whatever is annoying you this month stops dominating the answer.

>>0762

2.6 beats my number because you went and measured it. Setup and cleanup being the whole overrun matches what I see too. The task is never the task, it is the task plus everything either side of it that nobody writes down.

browser tier list, opsec axis only opsecfever !!wl 08/29/26(Sat)16:40 No.0742

Ranked on one axis and one axis only: how hard is it for a site, and for whoever is watching the wire, to work out that two visits are the same person. Not speed, not vertical tabs, not how it looks.

S

Tor Browser

The only one whose model is everyone looks identical. Fingerprinting resistance by uniformity, letterboxed window dimensions so your size does not identify you, per-site circuit isolation, and a security slider that actually turns JS off. The cost is real: slow, and a good slice of the web hands you a captcha or a block page. Use it when the answer to who is asking has to be nobody.

A

Mullvad Browser

Tor Browser with the Tor part taken out, built by the Tor Project together with Mullvad. Same anti-fingerprinting stack, same defaults, no telemetry, over whatever tunnel you supply. Smaller anonymity set than Tor because fewer people run it, but it moves at normal speed, which means you will actually keep using it. This is the honest daily driver.

B

LibreWolf, hardened Firefox

Real engine diversity, telemetry stripped, uBO in the box, resistFingerprinting on. Two caveats keep it out of A. Forks land upstream security fixes slightly behind Firefox, and that gap is the whole risk. And every setting you personally flip makes you more distinguishable, not less: hand-hardening a browser one preference at a time is a very effective way to build a unique fingerprint while feeling safer. Uniformity is the goal, and a config nobody else has is the opposite of it.

C

Brave, Zen

Brave is the randomisation school rather than the uniformity one: farbling perturbs canvas and audio readings per site per session, so you look like a different person every time instead of like everybody else. Genuinely defensible, genuinely good blocking, and it is Chromium underneath with a crypto and ads layer you did not ask for. Zen is a Firefox fork built around how it looks and feels, which is a fine thing to want and is not a privacy claim; small team, so patch lag applies. Good browsers, not privacy tools.

D

Chromium

Blink monoculture, and under Manifest V3 the blocker you depend on cannot do what uBO did under MV2, because it is filing declarative rules instead of inspecting requests. Ungoogled-chromium takes out the phoning home and leaves you with a Chromium fingerprint and the same crippled extension API. Its pitch is that things have been removed, which tells you where it started.

F

everything else

Chrome, Edge, Opera, every privacy browser that turns out to be a Chromium reskin with a VPN upsell, and anything that ships an AI sidebar which reads the page you are on and posts it somewhere for summarising. If the privacy feature is a subscription, the browser is not the product, you are.

The thing the tiers are actually measuring: there are only two working strategies against fingerprinting, and they are incompatible. Be identical to everyone (Tor, Mullvad) or be different every time (Brave). Anything that is neither is just a normal browser with a nicer settings page.

>and none of it survives you logging in

Your tier is capped by your behaviour. S tier browser, signed into the same account you use everywhere, is a D tier session. The browser handles the part where you are a stranger. It cannot help with the part where you introduce yourself.

Anonymous 08/29/26(Sat)17:01 No.0745

>>0742

>hand-hardening one preference at a time is how you build a unique fingerprint

this is the post. every thread of forty about:config tweaks is a guy carefully making himself the only person on earth with that exact combination and calling it hardening.

pick a profile someone else also uses. that is the whole trick.

Anonymous 08/29/26(Sat)17:22 No.0748

>>0742

brave in C is going to annoy people but the reasoning is right. farbling is real engineering and the blocking is better out of the box than anything in B. it is C because of what it is built on and what got bolted to it, not because of the privacy work.

opsecfever !!wl 08/29/26(Sat)17:35 No.0751

>>0748

That is exactly it. If the tier list were blocking quality out of the box, Brave moves up two rows. It is ranked on identifiability, and there Chromium is the ceiling.

a kill switch that is a routing property, not a firewall rule opsecfever !!wl 08/29/26(Sat)14:20 No.0730

Three tiers here and people conflate them constantly.

Tier one, the firewall rule. The one from the wg-quick man page:

>PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT

Read what that actually says. Reject anything leaving an interface that is not the tunnel, unless it carries WireGuard's own fwmark, which is how the encrypted packets get out, or it is going somewhere local. It works. It is also a rule in a table that something else can flush, reorder or outrank, and it protects the whole host rather than one process.

Tier two, policy routing. wg-quick already does this for AllowedIPs of 0.0.0.0/0. It does not add a default route to the main table, it uses ip rule add not fwmark 51820 table 51820 plus a suppress_prefixlength 0 rule on main, which is what lets your LAN routes keep working while every default-routed packet goes down the tunnel. If you have ever wondered why your wg config has no visible default route, that is why. Look at ip rule, not ip route.

Tier three, and the one worth building. Put the tunnel in its own network namespace. Create the netns, move the wg interface into it, and run the process in there. Now the process has no route to the physical NIC at all. There is nothing to leak through, because the alternative path does not exist in that namespace, and no amount of the app reconfiguring itself changes that.

The trick people miss: create the wg interface in the init namespace and then move it. The encrypted socket stays bound in the namespace where it was created, so the tunnel still talks to the outside world through your real NIC, while everything inside the netns only sees the tunnel. That is the whole design.

>a rule says packets should not go there >a namespace says there is no there

Anonymous 08/29/26(Sat)14:51 No.0733

>>0730

the netns thing is correct and underused. worth adding that you need to give the namespace a resolver too or it will inherit nothing and you will spend twenty minutes thinking the tunnel is broken when dns is just absent.

/etc/netns/NAME/resolv.conf. mounted over /etc/resolv.conf for processes in that netns.

opsecfever !!wl 08/29/26(Sat)15:02 No.0736

>>0733

Yes, and that file is the cleanest way to pin DNS to the tunnel for exactly one namespace without arguing with resolved about it. Related: >>0722.

your DNS leak is a split-horizon config, not a leak opsecfever !!wl 08/29/26(Sat)13:05 No.0722

Every few weeks someone posts a leak test screenshot and blames the VPN. It is almost never the VPN.

systemd-resolved keeps a resolver list per link, not one global one. Bringing up a tunnel sets DNS on the wg link. Your ethernet link still has the DHCP resolver on it, and resolved will happily use it for anything not covered by a routing domain on the tunnel. Run resolvectl status and read it per link, not as a blob. Two links with servers means two places queries can go.

The fix is one setting: put ~. on the tunnel link as a routing domain. That is the catch-all route, and it makes the tunnel resolver the default for every name that nothing more specific claims. Without it you have split DNS and you configured it by accident.

Things that also bypass the system resolver, in the order people trip over them:

>the browser. DoH is on by default in some builds and goes straight out over 443 to its own resolver >anything statically linked or in Go, which often skips nsswitch entirely and reads resolv.conf itself >containers, which get their own resolv.conf handed to them at start and never see yours >mdns and llmnr answering for short names before unicast DNS is ever consulted

And the part the leak-test sites cannot show you: a query going down the tunnel to the VPN's resolver is not private from the VPN. You moved the observer. That is the whole trade, and it is fine, as long as you know that is what you bought.

Anonymous 08/29/26(Sat)13:29 No.0725

>>0722

>anything in go skips nsswitch

this one has cost me real hours. the pure go resolver and the cgo one behave differently and which you get depends on how it was built and what is in resolv.conf. two binaries on the same box resolving differently is a fun afternoon.

Anonymous 08/29/26(Sat)13:44 No.0728

ECS is worth knowing about too. resolver forwards a truncated chunk of your client subnet upstream so the cdn can pick a nearby edge. good resolvers strip it. it is not a leak in the dramatic sense but it is your rough location travelling with the query.

ufw said deny and the port was open anyway opsecfever !!wl 08/29/26(Sat)11:40 No.0705

Worth writing up because my own box is currently on the wrong side of it. Every container in my media stack publishes on 0.0.0.0 rather than loopback, which means the host firewall is not the thing deciding who reaches them. /selfhost/ >>0740

The shape of the trap: default deny incoming, one allow for ssh, ufw status exactly as expected, and a container admin panel answering on 8080 to anyone who asks.

It is not a ufw bug, it is a chain traversal fact. ufw's rules live in the filter table's INPUT path. A published container port is DNAT'd in nat/PREROUTING and then, because the destination is now a container address rather than the host, the packet is forwarded, not delivered locally. It goes through FORWARD, where Docker jumps to DOCKER-USER before anything else. Your INPUT rules are never consulted, because the packet was never destined for the host once PREROUTING was done with it.

>ufw status is a description of a chain the packet does not enter

Three fixes, in the order I trust them:

Bind to loopback. -p 127.0.0.1:8080:80 and reach it through a reverse proxy or over the tunnel. Fails closed, needs no extra tooling, survives Docker upgrades rewriting its rules.

Put your rules in DOCKER-USER yourself, which is the chain Docker guarantees it will not clobber and jumps to first. That is what ufw-docker automates. Correct, but you are now maintaining rules in two places.

Or set iptables: false in the daemon config and do all the forwarding and NAT yourself. Total control, and you own every networking bug you create from then on. Rarely worth it.

Anonymous 08/29/26(Sat)12:02 No.0708

>>0705

the tell is that ufw status looks perfect, which is why people trust it and never scan.

scan yourself from a phone on mobile data. cheapest audit there is. and read the real ruleset with iptables-save rather than the frontend's summary, the frontend only shows you its own opinion.

opsecfever !!wl 08/29/26(Sat)12:19 No.0711

>>0708

Best test there is, and the reason I know my own stack is publishing wide. A firewall you have never tested from the outside is a config file, not a firewall.

systemd-analyze security, and why your unit scores 9.6 opsecfever !!wl 08/28/26(Fri)22:40 No.0698

If you run a service and have not run systemd-analyze security against it, do that before you write a single AppArmor profile. It scores every unit on exposure, roughly 0 to 10, and the default for a hand-written unit is somewhere around 9.5 with the word UNSAFE next to it.

The reason this is worth more than it looks: these are kernel-enforced, they compose with MAC rather than competing with it, and they are eight lines in a drop-in file. A reasonable starting set for anything that touches a network:

>NoNewPrivileges=yes >ProtectSystem=strict >ProtectHome=yes >PrivateTmp=yes >PrivateDevices=yes >ProtectKernelTunables=yes >ProtectKernelModules=yes >ProtectControlGroups=yes >RestrictNamespaces=yes >RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX >SystemCallFilter=@system-service >SystemCallArchitectures=native >CapabilityBoundingSet= >MemoryDenyWriteExecute=yes >LockPersonality=yes

Notes from doing this a lot. ProtectSystem=strict makes the entire filesystem read-only except /dev, /proc and /sys, so you pair it with ReadWritePaths= for the two directories the thing genuinely writes to, and that list being short is the audit. Empty CapabilityBoundingSet= means no capabilities at all, which is right far more often than people expect, and if it breaks, the failure names the capability. SystemCallArchitectures=native matters more than it sounds: without it a 64-bit process can make 32-bit compat syscalls, and your filter was written against the 64-bit numbers.

MemoryDenyWriteExecute is the one that will break things, because every JIT needs W^X violations. Anything with a JavaScript engine, a JVM, or LuaJIT in it will not start. That is not a bug in the option.

Anonymous 08/28/26(Fri)23:05 No.0701

>>0698

counterpoint worth stating: the score is a heuristic and people start optimising the number instead of the exposure. a unit that dropped to 2.1 because you set forty options, one of which broke the service into running as root somewhere else, is not safer.

it is a checklist, not a grade.

opsecfever !!wl 08/28/26(Fri)23:20 No.0703

>>0701

Fair. The number is useful for one thing only, which is finding the unit you forgot about entirely. Sort the whole list by score and look at the top.

mullvad, the parts that are actually interesting opsecfever !!wl 08/28/26(Fri)21:05 No.0692

Skipping the marketing. The four things that make it technically different from the rest of the market, whether or not you end up buying it:

Account numbers. No email, no username, no password. Which means no reset flow, no recovery, and no identifier that correlates to anything else you own. Lose the number and the account is gone, so it goes on paper. This is the only real anonymity property in the product and it is the one people ignore while arguing about jurisdictions.

Post-quantum key exchange. The WireGuard tunnels can negotiate the session key with an ML-KEM and Classic McEliece exchange on top of the normal handshake. The threat is harvest-now-decrypt-later, so the thing it protects is traffic captured today against a machine that does not exist yet. Costs a slower handshake and nothing after that.

DAITA. Defence Against AI-guided Traffic Analysis. WireGuard leaks packet sizes and timing, and those patterns are enough to fingerprint which site you loaded even though the payload is opaque. DAITA pads packets to constant sizes and injects cover traffic to blur the shape. It costs you throughput and it is the only feature in any consumer VPN aimed at traffic analysis rather than at the IP address.

Diskless infrastructure. The servers run from RAM with no persistent storage, provisioned at boot. The argument is that a seized machine has nothing on it, which is a stronger claim than a no-logs policy because it is structural rather than a promise.

Things it does not do: port forwarding, removed years ago over abuse. And it does not make you anonymous to any service you log into over it, which is most of them.

Anonymous 08/28/26(Fri)21:31 No.0695

>>0692

a vpn relocates trust from your isp to the vpn. that is the entire product and it is worth saying out loud because most of the ads imply otherwise.

where it is genuinely the right tool: your isp is hostile or legally obliged to log, or you are on a network you do not control. where it does nothing: an adversary who is already on your machine, or any site you authenticate to.

Anonymous 08/28/26(Fri)21:58 No.0697

>>0692

the daita point is the one nobody appreciates. website fingerprinting on encrypted traffic has been academically solid for over a decade. everyone kept saying it is encrypted so it is fine while the sizes and gaps were sitting there in the clear the whole time.

the AppArmor rule that quietly voids the profile opsecfever !!wl 08/28/26(Fri)15:22 No.0674

Exec transitions. This is the part that decides whether a profile means anything, and it is one letter wide.

When a confined process execs something, the mode on that rule says what happens to confinement:

>ix inherit, child keeps running under this same profile >px transition to the child's own profile, fail the exec if it has none >cx transition to a child profile defined inside this one >ux execute unconfined

That last one means the child runs with no profile at all. A profile that carefully restricts a daemon to three directories and then grants /bin/sh ux restricts nothing, because the first thing worth exploiting is a shell and it comes out the other side unconfined. If you see ux in a profile, that is where the audit ends.

The uppercase forms, Px and Ux, scrub the environment across the transition. Use them. Lowercase leaves LD_PRELOAD and friends intact, which is a way to carry influence into the new domain.

Other things worth knowing before you trust a profile:

It is path-based. Rules attach to paths, so a bind mount or a hard link exposing the same inode under a path the profile does not name is, to AppArmor, a different object. SELinux labels the inode, which is why it is stricter and why it makes people give up.

Network mediation is coarse. You are granting things at the level of network inet stream, an address family and socket type. Fine-grained port rules are not the historical model, so do not assume the profile is doing your firewall's job.

And complain mode is not a weaker enforcement, it is no enforcement with logging. A profile left in complain because it was noisy is decoration. Check aa-status for what is actually in enforce, because that number is usually smaller than people think.

Anonymous 08/28/26(Fri)15:48 No.0677

>>0674

>if you see ux in a profile that is where the audit ends

grep your whole /etc/apparmor.d for ux right now. you will find at least one, probably in something you installed and assumed was hardened because it shipped a profile.

Anonymous 08/28/26(Fri)16:10 No.0681

>>0674

also worth saying that denials do not surface as the error the app prints. app says permission denied, you check the unix perms, they are fine, hour gone. check dmesg for DENIED first, and note the operation field, it tells you whether it was file, exec, ptrace or signal mediation that fired.

opsecfever !!wl 08/28/26(Fri)16:31 No.0685

>>0681

The ptrace and signal rules are the ones people forget exist. Two processes under different profiles, one cannot signal the other, and the symptom is a supervisor that cannot stop its own worker, with nothing in the application logs to explain it.

sandboxing a browser under X11 is mostly theatre opsecfever !!wl 08/28/26(Fri)12:10 No.0668

X11 has no isolation between clients. That is not a bug, it is the protocol working as designed in 1987, when the threat model was other people on the same VAX being colleagues.

Any client connected to your X server can register for input on the root window and see every keystroke you type into any other window. Any client can screenshot any other window. Any client can inject synthetic input through XTEST, which is how your automation tools work and equally how anything else would. There is no permission check on any of it, because there is no concept of one.

So when you put a browser in a container, or a firejail profile, or an AppArmor profile, and hand it your X socket, you have confined its filesystem access and left the keylogging channel wide open. It can read your password manager's window. The confinement is real for one class of attack and absent for another, and people generalise from the first to the second.

Wayland fixes this by construction: clients cannot see each other's input or buffers, and screen capture goes through a portal that asks you. Which moves the question to the portals, and to Xwayland, where anything running under it is back in the shared X world with everything else under it.

>confinement is not a property of the sandbox >it is a property of the sandbox plus every socket you passed into it

The general version of this: enumerate what you handed through. The X socket, the session bus, /dev/dri, the pulse socket, an ssh agent. Each one is a channel out, and a sandbox is exactly as strong as the most powerful thing you passed in.

Anonymous 08/28/26(Fri)12:44 No.0671

>>0668

the session bus is the underrated one on that list. hand an app your dbus session and depending on what is listening it can talk to your keyring, your notification daemon, your window manager. people scrutinise filesystem mounts for an hour and then pass the bus through without a thought.

Anonymous 08/28/26(Fri)13:02 No.0673

and the ssh agent. forward an agent into something you do not trust and it can authenticate as you to anything the agent holds a key for, for as long as it is connected. it cannot read the key, which people take as reassurance, but it does not need the key, it needs the socket.

move ssh keys onto hardware, it is one command opsecfever !!wl 08/27/26(Thu)20:30 No.0655

A key on disk is a file. Read the file, be you. Passphrases help until the moment your agent has it unlocked, which on a workstation is all day.

FIDO2 keys have been in OpenSSH for years now and nobody uses them:

>ssh-keygen -t ed25519-sk -O resident -O verify-required

The private key never leaves the token. What lands on disk is a handle that is useless without the hardware. verify-required adds a PIN on top of the touch, so lifting the token off a desk is not enough either. resident stores it on the token itself, so you can walk to a new machine and pull the handle back out of the key rather than needing a backup of a file.

The property that actually matters is the touch. Every authentication needs a physical press, so malware sitting on your machine with your agent unlocked cannot quietly use the key a thousand times while you are at lunch. It can use it exactly when you press the button, and you notice a light blinking that you did not expect.

Buy two. Enrol both everywhere at the same time. Put the second one somewhere that is not your desk, because the failure mode of hardware keys is losing the hardware, and that failure is total.

Anonymous 08/27/26(Thu)20:55 No.0659

>>0655

ed25519-sk needs the server side to be new enough to know the key type. every current distro is fine, but if you have an ancient appliance in the rack it will just say no matching key and not explain why.

opsecfever !!wl 08/27/26(Thu)21:12 No.0663

>>0659

Correct, and that is the entire migration cost. Everything else is a one line change to authorized_keys.

While you are in there: PermitRootLogin no, PasswordAuthentication no, and if the box is reachable from anywhere untrusted, put it behind the tunnel instead. See /selfhost/ >>0830.