DEV Community

Cover image for My Firewall Is Off, 42 of 55 Sockets Unreachable from the Network
Mustafa ERBAY
Mustafa ERBAY

Posted on Originally published at mustafaerbay.com.tr

My Firewall Is Off, 42 of 55 Sockets Unreachable from the Network

The question was as harmless as a coffee break: is the firewall on this machine
enabled? The known answer runs through a plist file: read the globalstate key,
0 means off, 1 means on. It is settled enough practice that Apple's own release
notes speak to workflows built on modifying that file. I typed it into the
terminal and got back "no such file".

The file is not on this machine. Apple took the settings out of the plist with
macOS 15; exactly when the file itself disappeared is something neither the
release note states nor I can measure. More interesting still, when I asked the
same question in three different places I got three different answers. And in
the end this came out: the question I was asking was wrong too.

Every measurement was taken on the evening of 21 October 2026, on a single
machine: macOS 26.6.2 (build 25G83), arm64, no MDM enrollment
(profiles status → "MDM enrollment: No"). Every command is read-only; I did
not change a single setting on this machine for this article. The numbers are a
snapshot and they drift as the machine runs — what matters is not the
individual figures but the ratio between them.

The file is gone, the tool still answers

The known path is /Library/Preferences/com.apple.alf.plist.
"alf" stands for Application Firewall — the long-standing internal name of
macOS's application-based firewall. I asked about the file:

$ stat /Library/Preferences/com.apple.alf.plist
stat: /Library/Preferences/com.apple.alf.plist: stat: No such file or directory
Enter fullscreen mode Exit fullscreen mode

(Yes, stat: is printed twice — that is BSD stat's own habit, and you will
see it too.) The detail here matters: the error is not "Operation not permitted", it is "No
such file or directory". On macOS, trying to read a protected file gives you a
permission error; a missing file is a different message. So this is not an
access barrier — the file genuinely is not there.

What does the tool say, then? The binary carrying the same name is still in
place and — the part that surprised me — it answers without sudo:

$ /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
Firewall is disabled. (State = 0)
$ ... --getblockall
Firewall has block all state set to disabled.
$ ... --getstealthmode
Firewall stealth mode is off
$ ... --getallowsigned
Automatically allow built-in signed software ENABLED.
Automatically allow downloaded signed software ENABLED.
Enter fullscreen mode Exit fullscreen mode

Four questions, four answers, no root. That is a small security note in its own
right: a process running on my machine with ordinary user privileges can read
the state of my firewall without being asked for a single permission.

Apple wrote down where the gap comes from, in its own release notes. Under the
"Application Firewall" heading of the macOS Sequoia 15 developer notes, in the
"Deprecations" section, there is a single item, and it names the file exactly:
the settings are no longer held in a property list, and any app or workflow that
worked by modifying that file needs to move to the socketfilterfw command-line
tool.

So the check in circulation is not stale, it is retired. And the only way to
find that out is to ask for the file one more time; nobody sends you a notice.

So where does the state live?

I wanted to chase this, because in a security review "the tool says so" is not
a sufficient answer; to verify something you need to know where you are
looking. I scanned the strings inside the binary: socketfilterfw carries two
plist paths — one is the factory template I will open in a moment, the other is
the no-longer-existing /Library/Preferences/com.apple.alf.plist. A path
occurring inside a binary does not mean the binary reads it; but a retired path
still sitting there shows the distance between the tool's history and its
present.

In the places I can read, I could not find the new location: there is no
firewall-related file under /Library/Preferences, /Library/Managed
Preferences
is empty, and defaults read com.apple.alf and
defaults read com.apple.security.firewall do not come back empty — they come
back with an error: Domain com.apple.security.firewall does not exist.
pfctl -s info wants root; macOS does ship pf, a real packet filter, but that
is another evening's subject.

What ended the search was an answer from an Apple engineer. In a developer forum
thread asking exactly my question
(thread 760135), the reply
from Apple's Developer Technical Support team is short: the location and format
of these preferences is not considered API, so it will not be shared; and there
is no other known way to observe the setting. The developer who asked pressed
on: the release notes point at socketfilterfw, yet the same support team had
said elsewhere that socketfilterfw is not API either — so which is the proper
way? That message got no reply.

Keeping the state of a security control somewhere its own documentation never
names is a technical choice. But the practical consequence is this: you cannot
audit that control, you can only trust it. Those are not the same thing.

The template is not my machine

This was the part most likely to mislead me. The file is absent under
/Library/Preferences — but a file with the same name sits in the
/usr/libexec/ApplicationFirewall/ directory, 4,279 bytes, the factory
template that ships with the system (its version key reads 1.7). I opened
it, and the contents looked exactly like the answer I had got from the tool:
globalstate 0, stealthenabled 0, allowsignedenabled 1,
allowdownloadsignedenabled 1.

I could have stopped there and said "so the live state is in this file". It
would have been wrong, and the template itself says so. Its applications
array is completely empty. Yet the tool lists seven applications:

$ /usr/libexec/ApplicationFirewall/socketfilterfw --listapps
Total number of apps = 7
1 : /usr/libexec/remoted            (Allow incoming connections)
2 : /usr/bin/python3                (Allow incoming connections)
3 : /usr/bin/ruby                   (Allow incoming connections)
4 : /usr/sbin/cupsd                 (Allow incoming connections)
5 : /usr/libexec/sharingd           (Allow incoming connections)
6 : /usr/libexec/sshd-keygen-wrapper (Allow incoming connections)
7 : /usr/sbin/smbd                  (Allow incoming connections)
Enter fullscreen mode Exit fullscreen mode

I compared those seven paths against every list inside the template, one by
one. The template has three separate places: 10 paths under exceptions (all
carrying state: 3 — what that value means is not documented by Apple; the name
suggests an exemption, but I could not verify it), 7 bundle identifiers under
explicitauths (python, ruby, php, nc, ksh, a2p, java), and 10 named sharing
services. Of the seven paths the tool returned, only three (cupsd,
sshd-keygen-wrapper, smbd) match service names in the template. python3
and ruby appear in the template not as paths but as bundle identifiers inside
explicitauths — different form, different list. remoted and sharingd
appear in none of them.

Do not run this comparison with a plain grep: remoted occurs inside the
template's xartstorageremoted path, and sharingd occurs inside
screensharingd. Two words, two false positives, one wrong conclusion. Switch
to full-path matching and both disappear.

The conclusion: the tool is handing me a list it does not read from the
template. So there is live state, and I cannot read it. The template's numbers
agreeing with the tool's answer is not a confirmation but a coincidence — both
may simply be reporting Apple's default.

What "on" does not mean

This is where I got the real lesson. All this time I had been chasing the on/off
question, when that switch says far less than I assumed.

This macOS firewall is not port-based but application-based: it governs not
which port is closed but which program may accept incoming connections. And it
has two switches, both on by default. Apple's own device management
documentation states it plainly: for the AllowSigned and AllowSignedApp keys
in the com.apple.security.firewall payload, "the system ensures that the key
always has a value; if missing from the payload, the system sets it to true".
The first covers built-in signed software, the second downloaded signed
software.

Apple's user guide, on the page describing the settings, says the same thing in
everyday language: apps and services signed by a valid certificate authority are
added to the list of allowed apps without your authorization. An "Important"
note on the guide's companion page — the one describing the firewall itself —
completes the picture:
some applications that have access never appear in the list at all — system
apps, services, processes, and signed programs launched automatically by other
applications. If you want to block those, you have to add them to the list by
hand.

Translate that to my machine. What would happen if I turned the firewall on?
Built-in signed software would go on accepting incoming connections. So would
the signed software I downloaded. What remains — unsigned programs, ones I
blocked by hand, or ones it would ask me about — is a small set. So the "on"
position is not the wall I had imagined; it is more like a doorkeeper who holds
the door for anyone carrying a signature.

There is stealth mode too: in Apple's words, the machine not responding to
probing requests that could reveal its existence. Mine is off. It is useful,
but not answering probes is not the same as not listening, and those two are
easy to conflate.

The real number is in the bind address

I changed the question. Instead of "is the firewall on?": how many sockets on
this machine are waiting for incoming connections, and can they be reached from
the network?

$ netstat -an -p tcp | awk '$NF=="LISTEN"{print $4}' | sed 's/\.[0-9]*$//' | sort | uniq -c
  13 *
  37 127.0.0.1
   5 ::1
Enter fullscreen mode Exit fullscreen mode

Fifty-five listening sockets, 46 distinct ports. Thirty-seven bind to
127.0.0.1, five to ::1 — 42 sockets in total, across 39 distinct ports.
Thirteen sockets bind to *, that is to all interfaces; those come to 7
distinct ports (six of them open one row each on IPv4 and IPv6, one opens a
single row). There is no overlap between the two sets.

The meaning of the number 42 is independent of the firewall, and its source is
not a setting but a protocol rule. Section 3.2.1.3 of RFC 1122, listing the
special address forms, has a single sentence for loopback: addresses of this
form must not appear outside a host — in the item's own words, "MUST NOT appear
outside a host". On the IPv6 side the rule is more explicit: Section 2.5.3 of
RFC 4291 says a packet whose destination address is loopback must never be sent
outside of a single node and must never be forwarded by a router, and that "a
packet received on an interface with a destination address of loopback must be
dropped". RFC 6890, entering both blocks into the special-purpose address
registry, writes False into four of their attributes: cannot be a source,
cannot be a destination, not forwardable, not global. The one caveat is the
footnote under the IPv4 table — a few protocols have been granted exceptions to
that rule; the ::1/128 row carries no such footnote.

Let me be explicit about one thing: the number 42 is not a measurement, it is a
derivation. The command reports the bind address of each socket; the
unreachability follows from that address and from the rule above.

In practice that means this: for a process listening on 127.0.0.1:5432, the
firewall setting is irrelevant. A packet arriving from the network cannot reach
that socket, because a packet carrying that address is not forwarded on the
network — that is what the rule says, and what routers do.
That is the situation for 42 of my 55 sockets. My firewall is off, and for those
42 it makes no difference.

Diagram

The remaining 7 ports are my surface toward the local network — not toward the
internet, which I will come to shortly. I am not writing the port numbers here;
the service names in the --listapps output above already hint at them, and
adding the numbers gives the reader nothing. The useful part is not my list but
your own. Let me share the question I asked myself: for each of these seven
services, "does it need to be bound to all interfaces, or was it switched on
once and left there?"

The number 42 can fool you

On its own, the sentence "it is bound to loopback, so it is safe" is lazy. Let
me write down the objections.

A socket bound to loopback cannot be reached from the network, true. But put a
proxy bound to all interfaces in front of it and it becomes reachable. On a
developer machine running containers this is not an exception but the normal
arrangement: the service inside the container listens on its own loopback, a
port forwarder on the host side binds to *, and the two are joined.
netstat tells you the truth — but you only see the whole truth if you know
which process is holding each socket at that moment.

Second objection — the question I left hanging above: being bound to * does
not mean "open to the internet", it means "reachable from the network I am on".
There is a router in between, and I did not measure it anywhere in this article;
for most machines on a home or office network, NAT already meets an inbound
connection at the door. But IPv4 and IPv6 are not the same here: netstat
prints * in the address column for both — it keeps the distinction in the
Proto column (tcp4/tcp6/tcp46), and it is the one-liner above that throws
that column away. Yet if you hold a global IPv6 address there is no NAT in front
of that socket at all; what covers you is whatever stateful filter your router
keeps for IPv6 — most home routers default to blocking inbound there, but "most"
is not your device. One line, two different exposures. That is why the
* count is not an alarm but a list of questions.

Third objection: the number 42 holds for connections arriving from the network,
not for code running on the machine. Any process running here can connect to
127.0.0.1. "Loopback only" is a network boundary, not an authorization
boundary. It is no justification for leaving a locally running service without
authentication.

Fourth: I only counted TCP. UDP has no listening state — a socket is either bound
or it is not, and netstat's LISTEN column stays empty there. So this
measurement is not the whole surface, only the slice I was looking at.

Six commands, read-only

For anyone who wants to ask the same question of their own machine, here is the
sequence I verified working today — none of it changes anything, none of it needs
root:

  1. /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate — on or off. If you have a note that reads the old plist path, throw it away.
  2. ... --getallowsigned — if two lines come back, read both. If you see "ENABLED", know that turning the firewall on will not stop signed software.
  3. ... --listapps — do not mistake the list for a complete one; Apple's note says some programs with access never appear here.
  4. netstat -an -p tcp | awk '$NF=="LISTEN"{print $4}' | sed 's/\.[0-9]*$//' | sort | uniq -c — this is the real number. The figure on the * line is your surface toward the local network.
  5. lsof -nP -iTCP -sTCP:LISTEN — it tells you who holds the sockets, the missing half of the previous step. Do not add its rows up against netstat's: without root it names only your own processes and other users' sockets drop off the list with no error (48 rows against 55 on this machine). lsof also prints per file descriptor where netstat prints per socket, so one socket can take two rows.
  6. profiles status -type enrollment — if the machine is under MDM, you are not the owner of these settings; Apple's documented configuration surface is the com.apple.security.firewall profile payload.

Put steps four and five side by side and what is left is asking, one by one: why
is this port here? The first places to look are a sharing toggle (the Sharing
section of System Settings) and the service's own bind address; both can come up
empty, and then you are left searching by process name. Once you find it, writing
127.0.0.1 instead of * is a more definite move than turning the firewall on.
Every line whose answer is "I don't know" is the work of another evening.

And one more thing: because there is no supported way to observe the state, this
is not something you do once and forget. On a single machine the remedy is to
ask --getglobalstate on an interval and catch the change; across several
machines, hand ownership to a profile — if you enforce the
com.apple.security.firewall payload, at least you are the one keeping the
record.

What was not where I was looking

What I learned this evening is not that my firewall is off — the template that
ships with the system says globalstate 0 too, and for 42 of my sockets it
makes no difference. What I learned is that I can ask about the state of one
setting in three places and get three
different answers: the check in circulation points at a retired file, the
factory template is not my machine, and the list the tool hands me is one whose
incompleteness is written into its own documentation.

There is a shared fallacy underneath all of it: mistaking knowing the name of a
switch for knowing what the switch does. "The firewall is on" is a sentence;
"this socket is bound to 127.0.0.1" is a fact. The first points at a policy,
and the inside of that policy is full of defaults, signature exemptions and
invisible lists. The second rests on a protocol rule and depends on nobody's
default.

So the next time I wonder how open my machine is to the outside, my first
command will not be --getglobalstate. It will be netstat. The switch tells
me what I was trying to do; the bind address tells me what I did.

I have put similar questions to this machine before — there was not a single
address in the 1,945 rows of my download
ledger
,
and I owned 146 directories I could not
open
— and each
time what produced the answer was not my curiosity but finding the right
command.

Official Sources

Top comments (3)

Collapse
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

bro literally in your posts I've seen that you always give sources of information bro really others flex there posts by professionalism touch, but you just remain silent and cook your post actually professional like I know you may think What I mean I'm not good to explain my inner feeling but really bro in simple words I can explain is you are professional person with silent but deadly professional post the meaning of actual professional posts is your posts.

Collapse
 
merbayerp profile image
Mustafa ERBAY •

Bro, that genuinely means a lot to me! ❤️ Thank you for such kind words. And don’t worry, you explained your feelings perfectly. 😄

Honestly, there’s nothing particularly special about how I write. Most of my articles come from ordinary things I experience during the day. Sometimes it’s a problem I run into while working, sometimes something unexpected I discover in the terminal, and sometimes just a question that pops into my head.

I investigate, test things, make mistakes, learn something new, and then turn that experience into an article. That’s pretty much my entire writing process. 😂

I include sources because I believe readers should be able to question and verify what I share. I’m still learning every day, just like everyone else.

For me, the best part isn’t looking professional. It’s sharing something that might save another developer a few hours of frustration.

Really appreciate you, little bro. Keep that curiosity alive. We’re all learning from each other! ❤️🔥

Collapse
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

bro all thing good , but you said in last we will learn from each other bro what you will learn from me? if we compare each other then you are lion I'm maybe a mouse or more worse a ant

respect bro 🫡