{"id":1775,"date":"2025-02-28T17:37:38","date_gmt":"2025-02-28T17:37:38","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/02\/28\/docker-engine-v28-hardening-container-networking-by-default\/"},"modified":"2025-02-28T17:37:38","modified_gmt":"2025-02-28T17:37:38","slug":"docker-engine-v28-hardening-container-networking-by-default","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/02\/28\/docker-engine-v28-hardening-container-networking-by-default\/","title":{"rendered":"Docker Engine v28: Hardening Container Networking by Default"},"content":{"rendered":"<p>Docker simplifies containerization by removing runtime complexity and making app development seamless. With <a href=\"https:\/\/docs.docker.com\/engine\/release-notes\/28\/\" target=\"_blank\">Docker Engine v28<\/a>, we\u2019re taking another step forward in security by ensuring containers aren\u2019t unintentionally accessible from local networks. This update isn\u2019t about fixing a single vulnerability \u2014 it\u2019s about security hardening so your containers stay safe.\u00a0<\/p>\n<h2 class=\"wp-block-heading\">What happened?<\/h2>\n<p>When you run a container on the default Docker \u201cbridge\u201d network, Docker sets up <strong>NAT (Network Address Translation)<\/strong> rules using your system\u2019s firewall (via<a href=\"https:\/\/docs.docker.com\/engine\/network\/packet-filtering-firewalls\/\" target=\"_blank\"> iptables<\/a>). For example, the following command forwards traffic from port 8080 on your host to port 80 in the container.\u00a0<\/p>\n<p>docker run -d -p 8080:80 my-web-app<\/p>\n<p>However, if your host\u2019s filter-FORWARD chain is permissive (i.e., ACCEPT by default) <strong>and<\/strong> net.ipv4.ip_forward is enabled, unpublished ports could also be remotely accessible under certain conditions.<\/p>\n<p>This only affects hosts on the <strong>same physical\/link-layer network<\/strong> as the Docker host. In multi-tenant LAN environments or other shared local networks, someone connected on an RFC1918 subnet (such as 192.168.x.x or 10.x.x.x) could reach unpublished container ports if they knew (or guessed) its IP address.<\/p>\n<h2 class=\"wp-block-heading\">Who\u2019s affected?<\/h2>\n<p>This behavior only affects Linux users running Docker versions earlier than 28.0.0 with iptables. Docker Desktop is <strong>not<\/strong> affected.<\/p>\n<p>If you installed Docker on a single machine and used our defaults, without manually customizing your firewall settings, you\u2019re likely unaffected by upgrading. However, you could be impacted if:<\/p>\n<p>You <strong>deliberately set your host\u2019s FORWARD chain to ACCEPT<\/strong> and rely on accessing containers by their IP from other machines on the LAN.<\/p>\n<p>You <strong>bound containers to 127.0.0.1<\/strong> or another loopback interface but still route external traffic to them using advanced host networking tricks.<\/p>\n<p>You <strong>require direct container access<\/strong> from subnets, VLANs, or other broadcast domains <em>without<\/em> explicitly publishing ports (i.e., no p flags).<\/p>\n<p>If any of these exceptions apply to you, you might notice that containers previously reachable by direct IP now appear blocked unless you opt out or reconfigure your networking.<\/p>\n<h2 class=\"wp-block-heading\">What\u2019s the impact?<\/h2>\n<p>This exposure would\u2019ve required being <strong>on the same local network<\/strong> or otherwise having route-level access to the container\u2019s RFC1918 IP range. It <strong>did not<\/strong> affect machines across the public internet. However, a malicious user could discover unpublished container ports, within LAN settings or corporate environments, and connect to them.\u00a0<\/p>\n<p>For instance, they might add a custom route to the container\u2019s subnet, using the following command:<\/p>\n<p>ip route add 172.17.0.0\/16 via 192.168.0.10<\/p>\n<p>From there, if 192.168.0.10 is your Docker host with a permissive firewall, the attacker could send packets straight to 172.17.0.x (the container\u2019s IP).<\/p>\n<h2 class=\"wp-block-heading\">What are my next steps?<\/h2>\n<p>If you\u2019ve been impacted, we recommend taking the following three steps:<\/p>\n<h4 class=\"wp-block-heading\">1. Upgrade to Docker Engine 28.0<\/h4>\n<p>Docker Engine 28.0 now drops traffic to unpublished ports by default.\u00a0<\/p>\n<p>This \u201csecure by default\u201d approach prevents containers from being unexpectedly accessible on the LAN. Most users won\u2019t notice a difference, published ports will continue to function as usual (-p 8080:80), and unpublished ports remain private as intended.<\/p>\n<h4 class=\"wp-block-heading\">2. Decide if you need the old behavior (opt-out)<\/h4>\n<p>If you connect to containers over the LAN without publishing ports, you have a couple of options:<\/p>\n<p><strong>Option 1: Disable Docker\u2019s DROP policy<br \/><\/strong> In \/etc\/docker\/daemon.json, add the following:<\/p>\n<p>{<br \/>\n  &#8220;ip-forward-no-drop&#8221;: true<br \/>\n}<\/p>\n<p>Or you can run Docker with the &#8211;ip-forward-no-drop flag. This preserves a globally open FORWARD chain. But keep in mind that Docker still drops traffic for unpublished ports using separate rules. See Option 2 for a fully unprotected alternative.<\/p>\n<p><strong>Option 2: Create a \u201cnat-unprotected\u201d network<\/strong><\/p>\n<p>docker network create -d bridge \\<br \/>\n  -o com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected \\<br \/>\n  my_unprotected_net<\/p>\n<h4 class=\"wp-block-heading\">3. Consider custom iptables management<\/h4>\n<p>Advanced users with complex network setups can manually configure iptables to allow exactly the traffic they need. But this route is only recommended if you\u2019re comfortable managing firewall rules.<\/p>\n<h2 class=\"wp-block-heading\">Technical details<\/h2>\n<p>In previous Docker Engine versions, Docker\u2019s reliance on a permissive FORWARD chain meant that containers on the default bridge network could be reached if:<\/p>\n<p>net.ipv4.ip_forward was enabled (often auto-enabled by Docker if it wasn\u2019t already).<\/p>\n<p>The system-wide FORWARD chain was set to ACCEPT.<\/p>\n<p>Another machine on the same LAN (or an attached subnet) routed traffic to the container\u2019s IP address.<\/p>\n<p>In Docker 28.0, we now explicitly drop unsolicited inbound traffic to each container\u2019s internal IP unless that port was explicitly published (-p or &#8211;publish). This doesn\u2019t affect local connections from the Docker host itself, but it does block remote LAN connections to unpublished ports.<\/p>\n<h3 class=\"wp-block-heading\">Attacks: a real-world example<\/h3>\n<p>Suppose you have two hosts on the same RFC1918 subnet:<\/p>\n<p><strong>Attacker<\/strong> at 10.0.0.2<\/p>\n<p><strong>DockerHost<\/strong> at 10.0.0.3<\/p>\n<p>DockerHost runs a container with IP 172.17.0.2, and Docker\u2019s firewall policy is effectively \u201cACCEPT.\u201d In this situation, an attacker could run the following command:<\/p>\n<p>ip route add 172.17.0.2\/32 via 10.0.0.3<br \/>\nnc 172.17.0.2 3306<\/p>\n<p>If MySQL was listening on port 3306 in the container (unpublished), Docker might still forward that traffic. The container sees a connection from 10.0.0.2, and no authentication is enforced by Docker\u2019s network stack alone.\u00a0<\/p>\n<h3 class=\"wp-block-heading\">Mitigations in Docker Engine 28.0<\/h3>\n<p>By enforcing a drop rule for unpublished ports, Docker 28.0 now prevents the above scenario by default.<\/p>\n<p><strong>1. Default drop for unpublished ports<\/strong><\/p>\n<p>Ensures that local traffic to container IPs is discarded unless explicitly published.<\/p>\n<p><strong>2. Targeted adjustments to FORWARD policy<\/strong><\/p>\n<p>Originally, if Docker had to enable IP forwarding, it would set DROP by default. Now, Docker only applies that if necessary and extends the same logic to IPv6. You can opt-out with &#8211;ip-forward-no-drop or in the config file.<\/p>\n<p><strong>3. Unpublished ports stay private<\/strong><\/p>\n<p>The container remains accessible from the host itself, but not from other devices on the LAN (unless you intentionally choose \u201cnat-unprotected\u201d).<\/p>\n<h3 class=\"wp-block-heading\">Why we\u2019re making this change now<\/h3>\n<p>Some users have raised concerns about local-network exposure for years (see issues like<a href=\"https:\/\/github.com\/moby\/moby\/issues\/14041\" target=\"_blank\"> #14041<\/a> and<a href=\"https:\/\/github.com\/moby\/moby\/issues\/22054\" target=\"_blank\"> #22054<\/a>). But there\u2019s been a lot of change since then:<\/p>\n<p><strong>Docker was simpler<\/strong>: Use cases often revolved around single-node setups where any extra exposure was mitigated by typical dev\/test workflows.<\/p>\n<p><strong>Ecosystem evolution<\/strong>: Overlay networks, multi-host orchestration, and advanced routing became mainstream. Suddenly, scenarios once relegated to specialized setups became common.<\/p>\n<p><strong>Security expectations<\/strong>: Docker now underpins critical workloads in complex environments. Everyone benefits from safer defaults, even if it means adjusting older assumptions.<\/p>\n<p>By introducing these changes in Docker Engine 28.0, we align with today\u2019s best practices: <strong>don\u2019t expose anything without explicit user intent<\/strong>. It\u2019s a shift away from relying on users to configure their own firewalls if they want to lock things down.<\/p>\n<h3 class=\"wp-block-heading\">Backward compatibility and the path forward<\/h3>\n<p>These changes are <strong>not<\/strong> backward compatible for users who rely on direct container IP access from a local LAN. For the majority, the new defaults lock down containers more securely without breaking typical docker run -p scenarios.<\/p>\n<p>Still, we strongly advise upgrading to 28.0.1 or later so you can benefit from:<\/p>\n<p><strong>Safer defaults<\/strong>: Unpublished ports remain private unless you publish or explicitly opt out.<\/p>\n<p><strong>Clearer boundaries<\/strong>: Published vs. unpublished ports are now unambiguously enforced by iptables rules.<\/p>\n<p><strong>Easier management<\/strong>: Users can adopt the new defaults without becoming iptables experts.<\/p>\n<h3 class=\"wp-block-heading\">Quick upgrade checklist<\/h3>\n<p>Before you upgrade, here\u2019s a recommended checklist to run through to make sure you\u2019re set up for success with the latest release:<\/p>\n<p><strong>Examine current firewall settings<\/strong><\/p>\n<p>Run iptables -L -n and ip6tables -L -n. If you see ACCEPT in the FORWARD chain, and you depend on that for multi-subnet or direct container access, plan accordingly.<\/p>\n<p><strong>Test in a controlled environment<\/strong><strong><br \/><\/strong><strong><br \/><\/strong>Spin up Docker Engine 28.0 on a staging environment. Attempt to reach containers that you previously accessed directly. Then, verify which connections still work and which are now blocked.<\/p>\n<p><strong>Decide on opt-outs<\/strong><\/p>\n<p>If you do need the old behavior, set &#8220;ip-forward-no-drop&#8221;: true or use a \u201cnat-unprotected\u201d network. Otherwise, enjoy the heightened security defaults.<\/p>\n<p><strong>Monitor logs and metrics<\/strong><strong><br \/><\/strong><strong><br \/><\/strong>Watch for unexpected connection errors or service downtime. If something breaks, check if it\u2019s caused by Docker\u2019s new drop rules before rolling back any changes.<\/p>\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n<p>By hardening default networking rules in Docker Engine 28.0, we\u2019re reducing accidental container exposure on local networks. Most users can continue without disruption and can just enjoy the extra peace of mind. But if you rely on older behaviors, you can opt out or create specialized networks that bypass these rules.<\/p>\n<p><strong>Ready to upgrade?<\/strong> Follow our <a href=\"https:\/\/docs.docker.com\/engine\/install\/\" target=\"_blank\">official installation and upgrade instructions<\/a> to get Docker Engine 28.0 on your system today.<\/p>\n<p>We appreciate the feedback from the community and encourage you to reach out if you have questions or run into surprises after upgrading. You can find us on<a href=\"https:\/\/github.com\/moby\/moby\/issues\" target=\"_blank\"> GitHub issues<\/a>, our<a href=\"https:\/\/forums.docker.com\/\" target=\"_blank\"> community forums<\/a>, or your usual support channels.<\/p>\n<h3 class=\"wp-block-heading\">Learn more<\/h3>\n<p>Review the <a href=\"https:\/\/docs.docker.com\/engine\/release-notes\/28\/\" target=\"_blank\">Docker Engine v28 release notes<\/a><\/p>\n<p>Read Docker\u2019s<a href=\"https:\/\/docs.docker.com\/engine\/network\/packet-filtering-firewalls\/\" target=\"_blank\"> iptables documentation<\/a><\/p>\n<p>Issues that shaped these changes:<\/p>\n<p><a href=\"https:\/\/github.com\/moby\/moby\/issues\/14041\" target=\"_blank\">#14041<\/a><\/p>\n<p><a href=\"https:\/\/github.com\/moby\/moby\/issues\/22054\" target=\"_blank\">#22054<\/a><\/p>\n<p><a href=\"https:\/\/github.com\/moby\/moby\/issues\/48815\" target=\"_blank\">#48815<\/a><\/p>","protected":false},"excerpt":{"rendered":"<p>Docker simplifies containerization by removing runtime complexity and making app development seamless. With Docker Engine v28, we\u2019re taking another step [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[4],"tags":[],"class_list":["post-1775","post","type-post","status-publish","format-standard","hentry","category-docker"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1775","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=1775"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/1775\/revisions"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=1775"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=1775"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=1775"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}