<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hexward Labs</title>
    <description>The latest articles on DEV Community by Hexward Labs (hexwardlabs).</description>
    <link>https://dev.to/hexwardlabs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14865%2F1e31db06-8a92-4067-822a-47aba69f37a8.png</url>
      <title>DEV Community: Hexward Labs</title>
      <link>https://dev.to/hexwardlabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hexwardlabs"/>
    <language>en</language>
    <item>
      <title>Your attack surface is whatever the internet remembers — and it remembers more than you do</title>
      <dc:creator>Nizar Tuanku</dc:creator>
      <pubDate>Tue, 22 Sep 2026 03:22:04 +0000</pubDate>
      <link>https://dev.to/hexwardlabs/your-attack-surface-is-whatever-the-internet-remembers-and-it-remembers-more-than-you-do-1p08</link>
      <guid>https://dev.to/hexwardlabs/your-attack-surface-is-whatever-the-internet-remembers-and-it-remembers-more-than-you-do-1p08</guid>
      <description>&lt;h2&gt;
  
  
  The problem starts with something perfectly reasonable
&lt;/h2&gt;

&lt;p&gt;Someone spins up staging.company.com for a client demo. The demo goes well, the client is happy, everyone moves on to the next thing.&lt;br&gt;
That server is still running. It is still running the version of the application from eight months ago. Its admin panel still has the password someone picked in a hurry. And nobody in the company remembers it exists.&lt;br&gt;
No one was careless. This is how organisations normally work: things built to be temporary are rarely switched off, because switching them off was never anyone's job.&lt;br&gt;
Everything like that — exposed to the internet, reachable by outsiders — is what people mean by your attack surface.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why an attacker finds it before you do
&lt;/h2&gt;

&lt;p&gt;There is a difference in point of view that decides everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You see your infrastructure from the inside — from a server list, from documentation, from memory.&lt;/li&gt;
&lt;li&gt;An attacker sees it from the outside, and carries no assumptions about what is supposed to be there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are not looking for your important servers. They scan the whole internet, note what answers, and look for the weakest thing. That forgotten staging box is the ideal target precisely because it is real, connected to the same network, and watched by no one.&lt;/p&gt;
&lt;h2&gt;
  
  
  How forgotten assets can be found without guessing
&lt;/h2&gt;

&lt;p&gt;Here is where one little-known detail becomes decisive.&lt;br&gt;
Every time a site is issued an HTTPS certificate, the issuer is required to record it in a Certificate Transparency log — a public ledger anyone can read. The rule exists for a good reason: if someone issues a fraudulent certificate for your domain, the evidence is visible.&lt;/p&gt;

&lt;p&gt;The side effect: a company's list of subdomain names is often already public, with no guessing required. staging., vpn-old., backup., test2. — if it ever had a certificate, the name is in there.&lt;br&gt;
ASM reads that list, then checks which of those names are still actually alive. Anything that does not resolve is discarded. What remains is your real attack surface.&lt;/p&gt;
&lt;h2&gt;
  
  
  What gets checked once an asset is found
&lt;/h2&gt;

&lt;p&gt;On assets you have proven you own, ASM checks the things that most often turn out to be real ways in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Databases reachable from the internet. PostgreSQL, MySQL, MongoDB, Redis. A database should only be reachable by your application, not by anyone on the internet. This shows up far more often than people expect — usually because of one firewall rule added for debugging and never removed.&lt;/li&gt;
&lt;li&gt;Open remote access. RDP, VNC or Telnet facing the internet is a standing invitation to try passwords over and over.&lt;/li&gt;
&lt;li&gt;Every other open port, so you know what is actually answering.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The part that makes it useful every day: what changed
&lt;/h2&gt;

&lt;p&gt;Scanning once produces a long list that makes people give up.&lt;br&gt;
What is genuinely useful is the difference from yesterday:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new subdomain appears → a new finding&lt;/li&gt;
&lt;li&gt;A port that used to be closed is now open → a new finding&lt;/li&gt;
&lt;li&gt;A port you closed → the finding disappears on its own&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So after the first day you are no longer looking at a long list, but at a short list of things that changed. That is something a real person can work through each morning.&lt;/p&gt;
&lt;h2&gt;
  
  
  One limit you should know before you judge the results
&lt;/h2&gt;

&lt;p&gt;When we ran ASM against our own domain, hexwardlabs.com, it returned one asset — even though bridge.hexwardlabs.com is plainly alive and reachable by anyone.&lt;br&gt;
Why? Because that domain uses a wildcard certificate — a single *.hexwardlabs.com certificate covering every subdomain at once.&lt;/p&gt;

&lt;p&gt;So there was nothing to find from that source, and ASM does not guess names.&lt;br&gt;
We put this up front rather than hiding it, because if you run it against a wildcard domain and see a small number, you deserve to know why. A small number on a domain like that is not evidence that your surface is small.&lt;br&gt;
Since 5 September, ASM tells you this itself: when it detects a wildcard certificate it raises a coverage note before showing the inventory, so the report admits its own blind spot instead of letting a low number speak for itself. That note is filed as informational, not as a problem with your setup — owning a wildcard certificate is perfectly sensible. The limitation is ours, and it would be wrong to score you for it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why ASM refuses to scan until you prove ownership
&lt;/h2&gt;

&lt;p&gt;Before it scans anything, ASM asks for proof that the domain is yours — via a single DNS TXT record, or a file on that server.&lt;br&gt;
This is not a formality. Port-scanning someone else's systems without permission is a legal problem in many countries, and a tool that scans whatever its user types in is a tool that sooner or later gets used for exactly that.&lt;br&gt;
The boundary is permanent and cannot be switched off.&lt;/p&gt;
&lt;h2&gt;
  
  
  What changes once you are using it
&lt;/h2&gt;

&lt;p&gt;Before: you have a list of the servers you remember, and you hope it is complete.&lt;br&gt;
After: you have a list of what is provably alive and visible from outside, plus a notice every time something is added to it.&lt;br&gt;
That sounds like a small change, right up until the day the thing that was added is not something you built.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it yourself — 15 minutes
&lt;/h2&gt;

&lt;p&gt;The free Apache-2.0 edition on GitHub runs the same engine, one domain, with no time limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/nizartuanku/attack-surface-monitor/releases/latest/download/asm-free-0.1.1-linux-amd64.tar.gz
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/nizartuanku/attack-surface-monitor/releases/latest/download/SHA256SUMS
&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; SHA256SUMS
&lt;span class="nb"&gt;tar &lt;/span&gt;xzf asm-free-0.1.1-linux-amd64.tar.gz &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./asm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open 127.0.0.1:8423, enter one domain you own, publish its verification TXT record, and see what comes back.&lt;br&gt;
If what comes back surprises you — that is rather the point.&lt;br&gt;
Nizar Tuanku — Cybersecurity. · github.com/nizartuanku/attack-surface-monitor&lt;/p&gt;

&lt;h2&gt;
  
  
  Terms used above
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Certificate Transparency (CT) — a public list of every HTTPS certificate ever issued. Readable by anyone, including you, and including an attacker.&lt;/li&gt;
&lt;li&gt;Wildcard certificate — one certificate for all subdomains. Convenient for the administrator, but it means no individual subdomain name has ever appeared in a CT log.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article is the &lt;code&gt;docs/CONCEPTS.md&lt;/code&gt; of &lt;strong&gt;attack-surface-monitor&lt;/strong&gt;, one of the Hexward self-hosted security tools. The free edition is on GitHub under Apache-2.0 with no time limit: &lt;a href="https://github.com/nizartuanku/attack-surface-monitor" rel="noopener noreferrer"&gt;https://github.com/nizartuanku/attack-surface-monitor&lt;/a&gt; — the README has the install block, and the concepts above are the reasoning behind it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nizar Tuanku — Cybersecurity.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>networking</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Central NAT on FortiGate: what changes, what breaks, and the order you have to do it in</title>
      <dc:creator>Nizar Tuanku</dc:creator>
      <pubDate>Mon, 21 Sep 2026 23:45:08 +0000</pubDate>
      <link>https://dev.to/hexwardlabs/central-nat-on-fortigate-what-changes-what-breaks-and-the-order-you-have-to-do-it-in-42nf</link>
      <guid>https://dev.to/hexwardlabs/central-nat-on-fortigate-what-changes-what-breaks-and-the-order-you-have-to-do-it-in-42nf</guid>
      <description>&lt;p&gt;&lt;em&gt;First note in a series on cross-vendor firewall migration: where two platforms disagree, verified against vendor docs and real migrations.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you are moving a policy set onto FortiGate from an ASA or a Palo Alto, there is a decision you have to make before you write a single rule, and FortiOS will not let you unmake it afterwards without tearing the policies down.&lt;/p&gt;

&lt;p&gt;This is about central NAT. Not what it is — the documentation covers that — but what it does to a migration, and why it has to be the first thing you decide rather than the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why migrations run into this at all
&lt;/h2&gt;

&lt;p&gt;Per-policy NAT is the FortiGate default, and for a greenfield FortiGate it is usually the right choice. Each firewall policy carries its own &lt;code&gt;nat enable&lt;/code&gt; and optionally an IP pool. Simple, local, readable.&lt;/p&gt;

&lt;p&gt;The trouble is that neither an ASA nor a PAN-OS policy set is shaped that way.&lt;/p&gt;

&lt;p&gt;On an ASA, NAT lives in its own ordered table — object NAT and twice NAT, evaluated in sections, entirely separate from the ACLs. On PAN-OS, NAT rules are their own rulebase, also separate from security rules. In both, one NAT rule serves many policy rules.&lt;/p&gt;

&lt;p&gt;Per-policy NAT has no way to express that. If a single ASA twice-NAT statement covers traffic matched by fourteen ACEs, per-policy NAT makes you write the translation fourteen times, once on each converted policy. It works, and it is a maintenance problem the day you have to change it.&lt;/p&gt;

&lt;p&gt;Central NAT restores the shape you came from: one ordered SNAT table, one DNAT table, and firewall policies that only decide allow or deny. That is why converted configurations tend to land in central NAT, and why anyone doing this by hand usually ends up there too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order problem — read this one first
&lt;/h2&gt;

&lt;p&gt;You cannot switch to central NAT on a FortiGate that already has policies referencing VIPs or IP pools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cisco_ios"&gt;&lt;code&gt;&lt;span class="k"&gt;FGT&lt;/span&gt; # config sys settings
&lt;span class="k"&gt;FGT&lt;/span&gt; (settings) # set central-nat enable
&lt;span class="k"&gt;Cannot&lt;/span&gt; enable central-nat with firewall policy using vip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is an equivalent error for IP pools, and it names the policy ID. Every VIP and every IP pool has to reach zero references before the toggle will take. Not deleted necessarily — dereferenced — but on a freshly built policy set that usually amounts to the same thing.&lt;/p&gt;

&lt;p&gt;So the failure mode is this. You spend two days building out interfaces, zones, address objects, service objects and four hundred policies. You get to NAT, decide central NAT is the right model, and FortiOS refuses. Now you are editing four hundred policies to strip VIP references before you can enable the mode you should have enabled on an empty box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decide central NAT before you build the policy set.&lt;/strong&gt; On a new appliance the sequence is: enable central NAT, log out and back in so the GUI redraws, then build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What flips underneath you: the destination address
&lt;/h2&gt;

&lt;p&gt;This is the one that costs a cut-over window, because it fails in a direction that looks like a routing problem.&lt;/p&gt;

&lt;p&gt;With per-policy NAT, an inbound policy references the &lt;strong&gt;VIP object&lt;/strong&gt; as its destination address. That is the familiar pattern and it matches how an ASA or PAN-OS engineer thinks: the rule talks about the public address.&lt;/p&gt;

&lt;p&gt;With central NAT enabled, that is wrong. The DNAT and Virtual IPs table applies to every policy on the FortiGate, whether or not the policy mentions a VIP. Destination translation has already happened by the time the policy is evaluated. So the policy must reference the VIP's &lt;strong&gt;mapped address&lt;/strong&gt; — the internal one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Policy destination address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FortiGate, per-policy NAT&lt;/td&gt;
&lt;td&gt;the VIP object (public address)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FortiGate, central NAT&lt;/td&gt;
&lt;td&gt;the mapped address (internal address)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cisco ASA&lt;/td&gt;
&lt;td&gt;real address, post-8.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PAN-OS security rule&lt;/td&gt;
&lt;td&gt;pre-NAT address, post-NAT zone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four platforms, and the answer is different on each. PAN-OS is the cruellest of them — pre-NAT address but post-NAT zone in the same rule — and if you have spent years there, "destination is the public IP" is muscle memory. On a central-NAT FortiGate that muscle memory silently denies every inbound session.&lt;/p&gt;

&lt;p&gt;Worth saying plainly: under central NAT, &lt;strong&gt;all VIP/DNAT rules are in effect regardless of the central SNAT table&lt;/strong&gt;. The two tables are independent. A VIP you created and forgot about is live the moment central NAT is on, with no policy referencing it anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Central SNAT is first-match, and your old order does not carry over
&lt;/h2&gt;

&lt;p&gt;The central SNAT table is read top down and the first match wins. Same as an ACL, same as an ASA NAT section — but the &lt;em&gt;match keys&lt;/em&gt; are not the same, and that is where converted tables go wrong.&lt;/p&gt;

&lt;p&gt;A central SNAT entry matches on source interface, destination interface, source address, destination address, protocol, destination port and original source port. An ASA NAT rule has no concept of destination interface as a match key in the same way, and section ordering in ASA (manual NAT before auto NAT before after-auto NAT) does not map onto a single flat list.&lt;/p&gt;

&lt;p&gt;The practical consequence: an ASA NAT table that was correct in its own ordering can be flattened into a central SNAT table that is also individually correct rule by rule, and still behave differently, because the first-match boundary now falls somewhere else.&lt;/p&gt;

&lt;p&gt;The specific thing to check is your catch-all. Most estates end with a broad "everything out to the internet, hide behind the interface address" rule. If a narrower rule that used to be evaluated in an earlier ASA section ends up &lt;em&gt;below&lt;/em&gt; that catch-all in the flat table, it never matches, and the traffic silently leaves with the wrong source address. Nothing breaks. Nothing logs an error. A far-end ACL somewhere starts dropping you a week later.&lt;/p&gt;

&lt;p&gt;Read the table bottom-up looking for anything below your broadest rule. That is a five-minute check and it is the highest-yield five minutes in the whole exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explicit port mapping and the ICMP trap
&lt;/h2&gt;

&lt;p&gt;Central SNAT lets you pin an original source port range to a translated port range — &lt;code&gt;set orig-port&lt;/code&gt; and &lt;code&gt;set nat-port&lt;/code&gt;. Two constraints come with it:&lt;/p&gt;

&lt;p&gt;Only the &lt;strong&gt;Overload&lt;/strong&gt; IP pool type supports it. And any protocol without ports — ICMP first among them — &lt;strong&gt;cannot match a rule that uses explicit port mapping&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the rule you wrote to pin a port range for one application quietly does not apply to ping, traceroute or path-MTU discovery from the same hosts. That traffic falls through to whatever rule comes next, which is usually the interface-address catch-all, which is usually a different source address.&lt;/p&gt;

&lt;p&gt;You will find this the first time someone asks why the monitoring system sees a different source IP for ICMP than for TCP from the same server. Better to know before they ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more that catch people
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NGFW policy-based mode implies central NAT.&lt;/strong&gt; If the VDOM is in policy-based NGFW mode, central SNAT is on implicitly. You do not get to choose. If you are converting into a policy-based VDOM, you are in central NAT whether you planned for it or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With VDOMs, the SNAT and DNAT tables are per-VDOM, not global.&lt;/strong&gt; Obvious once said, easy to assume otherwise when you are consolidating several ASA contexts into VDOMs on one chassis and thinking of the NAT table as a property of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-cut-over checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Central NAT decided and enabled &lt;strong&gt;before&lt;/strong&gt; the policy set is built, on an empty box&lt;/li&gt;
&lt;li&gt;Logged out and back in so the GUI shows the Central SNAT and DNAT tables&lt;/li&gt;
&lt;li&gt;Every inbound policy references the &lt;strong&gt;mapped&lt;/strong&gt; address, not the VIP object&lt;/li&gt;
&lt;li&gt;VIP table audited — under central NAT everything in it is live, referenced or not&lt;/li&gt;
&lt;li&gt;Central SNAT table read bottom-up: nothing meaningful sits below the broad catch-all&lt;/li&gt;
&lt;li&gt;Any rule with explicit port mapping checked against the ICMP behaviour above, and the fall-through rule confirmed to be acceptable&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;diagnose sys session list&lt;/code&gt; on real traffic before you call it done — the &lt;code&gt;hook=post dir=org act=snat&lt;/code&gt; line tells you the actual translated address, which is the only answer that counts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last one is worth more than any amount of reading the config. The session table does not have opinions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I cannot check for you
&lt;/h2&gt;

&lt;p&gt;Everything above is behaviour I have hit on real migrations and then confirmed against the FortiOS documentation. What I cannot tell you is which of it applies to your build — central NAT behaviour has moved between major versions, and the version you are landing on matters.&lt;/p&gt;

&lt;p&gt;Verify against your own lab before your change window. The cost of being wrong here is not a config error, it is a cut-over that half works.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by someone who does firewall migrations for a living, after doing this one the hard way. Corrections welcome — if your experience differs on any of the above, I would rather hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; FortiOS 7.4.3 Administration Guide, &lt;em&gt;Central SNAT&lt;/em&gt; · Fortinet community article 181805, &lt;em&gt;cannot enable central-nat error&lt;/em&gt; · Fortinet community article 97658, &lt;em&gt;Configuration changes regarding Central NAT and Virtual IPs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;More notes like this, as they get written: &lt;a href="https://github.com/nizartuanku/firewall-migration-notes" rel="noopener noreferrer"&gt;github.com/nizartuanku/firewall-migration-notes&lt;/a&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>security</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
