Bind to IP or interface?

I've started improving the security of my network and created multiple VLANs for IOT, guest, trusted access, etc. I want to create a camera VLAN. Is there a way to bind SecuritySpy to an IP or an interface or will it always listen on all addresses?

If it can only listen on all addresses is there a way to limit access to the web interface to one subnet or would I need to employ a firewall solution?

Thanks folks!

Comments

  • Ben
    Ben
    edited December 2025

    For its web interface, SecuritySpy accepts incoming connections on any available network interface - there is currently no way to modify this behaviour, as that's what's best in most circumstances. If you need tighter restrictions, I hope you can find a solution, but if not please let us know - perhaps we can add this as an advanced setting.

  • +1 for being able to designate in SecSpy which interfaces it can bind to. That would be very helpful in terms of security and privacy.

  • This option has now been added in the latest beta version of SecuritySpy (currently 6.18b4) - you'll find it under Settings > Web > Advanced Web Options. Here you can choose which BSD network interface to listen on (e.g. en0, en1). Under a tagged VLAN system, you should see options here for each sub-interface (e.g. en0.10, en0.20). Please test and report back.

  • That's really cool Ben. I look forward to testing it. Thank you.

  • photonclock
    edited January 2

    @Ben I've tested the network interface binding.

    I had SecSpy accessible via tailscale, and via a cloudflared tunnel.

    Originally I had a cloudflared (zero trust dashboard) connector routing:

    mysecspysubnet.mydomain.com to localhost:[SEC_SPY_HTTP_PORT]

    With the 6.18b4 interface binding feature, I set SecSpy to bind to interface utun4 (my tailscale IP on the machine) and I changed the cloudflared connector to route to:

    mysecspysubnet.mydomain.com to [MY_TAILSCALE_IP]:[SEC_SPY_HTTP_PORT]

    I also enabled "Always use HTTPS" in the main cloudflare dashboard.

    From iPad with tailscale enabled: traffic is always encrypted on tailscale

    From iPad via web: https from iPad to cloudflare, https from cloudflare to cloudflared on my mac, to http interface on SecSpy. Seems safe enough...

    Then I realized this left me with one other problem. My Hubitat can no longer reach Security Spy because SecSpy is only listening on the utun4(tailscale) interface.

    So I setup a socat launchdaemon on the mac mini listening on another port, ie:

    socat TCP-LISTEN:HUBITAT_INCOMING_PORT,fork,bind=MAC_LAN_IP TCP:TAILSCALE_IP:SEC_SPY_HTTP_PORT

    So Hubitat calls the mac on en0/LAN address on a dedicated port, and socat redirects that to SecSpy listening on the tailscale interface...

    One thing I'm thinking I must test, as I write this, is if tailscale consistently binds to the same utun interface upon reboot?

    The downside is that...when I originally setup cloudflare, in addition to tailscale, the idea was that if one service went down, the other was reachable. With this config, if tailscale goes down, that's all she wrote...

    I realize it's a bit of UI work, but I think limiting the choice to only a single interface is a bit restrictive for multi-access setups. The above configuration more or less bypasses that restriction, but maybe it would be better if the user could choose one or more interfaces to listen to, ie, instead of a popup menu to choose one interface, a radio-button style selection interface that allows the user to multiple interfaces?

  • After a reboot, tailscale bound to utan4 again, so that seems promising.

    And socat launchdaemon bound to the incoming Hubitat port, allowing Hubitat to reach SecSpy...

    And SecSpy is reachable via web https and tailscale http...

    So...cool...seems like a viable config.

    But it might be easier for some folks (and me) if SecSpy could bind to any choice of interfaces?

  • After some research, I guess I can't expect tailscale to always bind to the utun4 interface.

    Could you make it possible for the user to also choose to bind via IP (vs interface name)?

    Consulting the AI, here's the discussion we had:

    (Reminder: My network and dev skills are in the class of "Golden Retriever" without AI assistance) 😁

    >> I did a reboot and tailscale bound to the utun4 interface. Can I expect that to be reliable?

    No, you cannot rely on it 100%.

    The interface name utun4 is dynamic. macOS assigns these numbers (utun0, utun1, utun2...) in the order that network services start up during boot.

    • Most of the time: If your startup items don't change, the order remains consistent, and it will stay utun4.
    • The Risk: If you install a new VPN, or if an Apple system service (like iCloud Private Relay or content filters) starts a split-second earlier than usual, Tailscale might get assigned utun3 or utun5.

    Why this breaks your setup

    Your socat bridge is perfectly safe because it targets the IP Address (100.xxx.xxx.xxx), which never changes. Your Security Spy setting is the weak point. If Security Spy is hard-coded to listen on "utun4", and Tailscale wakes up as "utun5", Security Spy will be listening to a dead (or non-existent) interface, and your cameras will go dark.

    The Fix / Mitigation

    1. The "Wait and See" Approach (Low Effort) Since you only reboot occasionally, just keep this in mind. If your cameras suddenly stop working after a reboot:

    1. Open Terminal: ifconfig | grep 100.xxx.xxx.xx
    2. See what interface it lists (e.g., utun5).
    3. Update Security Spy to match.

    2. The Feature Request (High Value) This is a beta feature (6.18b4). You should email Ben (the Security Spy developer).

    • Tell him: "I am using the new Interface Binding feature. I need to bind to my Tailscale VPN, but macOS changes the interface name (utunX) dynamically on reboot. Can you allow us to bind to a specific IP address instead of just an Interface Name?"
    • Why: Binding to 100.xxx.xxx.xxx is rock-solid and survives reboots. Binding to utun4 is brittle.

    Summary

    You are fine for now, but utun4 is a variable, not a constant. Your socat script is robust; Security Spy's beta setting is the only fragile link.

    >>how would the SecSpy developer have to implement that? he'd have to add business logic to search for a matching IP string in all interfaces, and then bind to the correct interface?

    Not necessarily. There is a much simpler, standard way.

    You are describing a "Lookup" method (find IP $\to$ get Interface Name $\to$ Bind), but standard socket programming allows you to skip the interface name entirely and bind directly to the IP.

    Here are the two ways Ben (the developer) could implement this. You can mention these in your email:

    Method 1: The "Standard" Way (bind to IP)

    This is the most robust method.

    In network programming, when you open a socket, you usually bind it to 0.0.0.0 (which means "listen on all interfaces").

    To limit it, he simply needs to change that 0.0.0.0 to your specific IP string (100.xxx.xxx.xxx).

    • Logic: He adds a text field to Preferences for "Listen IP."
    • Code: He passes that string into the bind() system call.
    • Result: The macOS kernel automatically routes traffic for that IP to his app. He never needs to know that utun4 exists. If utun4 changes to utun5, the IP moves with it, and the app keeps working without changing a single line of code.

    Method 2: The "Lookup" Way (What you described)

    This is required only if he insists on using a Dropdown Menu.

    If he wants to keep a UI where you pick from a list of hardware devices (rather than typing an IP), then yes, your logic is correct.

    • Logic: He would need to use getifaddrs().
    • Code:
      1. Call getifaddrs() to get a linked list of all network adapters.
      2. Iterate through them.
      3. Compare the assigned IP of each adapter to the "Target IP" you saved in settings.
      4. If match found $\to$ Get the if_index (Interface Index).
      5. Bind using setsockopt(..., IP_BOUND_IF, ...) or similar.

    Recommendation

    When you email Ben, suggest Method 1. It is standard practice for server software (like Nginx, Apache, or Python servers) to allow binding to a specific IP.

    Suggested phrasing:

    "Ideally, instead of binding to a specific Interface Name (like utun4), could you allow us to bind to a specific local IP address?

    If I could set Security Spy to bind specifically to 100.xxx.xxx.xx, standard socket bind() rules would handle the rest. This would allow the setting to survive reboots even if macOS re-assigns the Tailscale interface from utun4 to utun5."

  • After testing and thinking through all of that...I'm contemplating instead (at least for the time being until this SecSpy feature evolves):

    Bind SecSpy to en0

    Use socat to forward tailscale traffic incoming on SecSpy http PORT to en0 IP (because socat can bind via IP)

    That gives me back the flexibility of cloudflare and tailscale SecSpy access being totally independent.

    What I lose is SecSpy being entirely isolated from LAN traffic...which probably isn't that big of a risk. Well, these days everything is a risk, but...that's just goes to the issue of me upgrading to a Ubiquiti router before long, so I can VLAN everything...

    I don't know. 🤷🏼‍♂️ What do you think @Ben ?

  • We implemented this based on network interface BSD name (en0 etc.) to for neater UI and to make it easier for users (selecting an Interface is easier than entering IP addresses). But, your results suggest that simply allowing manually-entered IP addresses is actually the better solution. We could allow up to two IPs per service (HTTP, HTTPS), so this could also be useful in order to listen on two different networks. We'll make this change, but it may take a week or two. I'll post back here when there's a new beta to test.

  • THANK YOU BEN!

  • We now have an updated beta (6.18b5) to test with this tweaked functionality. Under Settings > Web > Advanced, you'll now find a text field called "Listen on IPs", where you can enter up to two IP addresses (comma-separated). If specified, the web server will listen on these IPs only, ignoring all others. This allows you to limit the availability of the server to specific subnets in a reliable way that doesn't depend on network interface names. Please test and report back.

  • @Ben This appears to be working as expected!

    The Test: With the prior "bind-to-interface" build, I had to use socat scripts to bridge my Tailscale traffic because I was forced to bind to en0 (LAN) to keep Hubitat happy. With this new "bind-to-IP" build:

    • SecSpy listens on 10.0.1.XXX (LAN) AND 100.XXX.XXX.XXX (Tailscale).
    • Hubitat talks to SecSpy on LAN natively.
    • Cloudflared relays public traffic to the LAN IP.
    • Result: Everything works natively. No socat or scripts needed. Much easier for my caveman brain. 😅

    One heads up to others: If you were using the previous beta, check your Web Settings immediately after updating, as you'll need to input your IPs to regain the restriction.

    Regarding the iOS App & Cloudflare: I'm still casting my vote for Custom HTTP Headers in the iOS app (support for Service Tokens). Since I'm now running this robust "Zero Trust" setup, allowing the SecSpy iOS/iPadOS apps to pass a CF-Access-Client-Id and Secret would be the final piece of the puzzle. It would allow SecSpy apps to seamlessly and conveniently "punch through" the Cloudflare protection on a user's trusted device without SecSpy server needing a full OIDC rewrite.

  • Great to hear this new feature works as expected! Thanks for the testing feedback, I hope this will be useful for you and others.