Run curl -4 https://qsa.sh on the cloud server. qsa.sh prints the public IPv4 address your request came from, waits 15 seconds, then scans that address from its own scanner network and streams back three things: the open TCP ports, the service and version on each port with known CVE matches, and nuclei findings. Put that list next to your security group or firewall rules and every difference between the two is a configuration you need to look at.
Run the scan from the server
curl -4 https://qsa.sh
The -4 flag forces IPv4. The qsa.sh hostname has IPv6 addresses, so curl on a dual-stack server can connect over IPv6, and qsa.sh refuses IPv6 callers for now. The https:// prefix encrypts the request; plain curl qsa.sh also works over HTTP. The command only prints text to your terminal, as explained in Is curl | bash safe?
Before any packet is sent, the output shows the detected IP and a 15-second countdown. Check that the IP is the server's public address; Ctrl-C cancels and nothing is scanned. qsa.sh scans only the IP the request comes from, so there is no target to type and no way to point it at another host.
If the server reaches the internet through a NAT gateway, the IP shown belongs to the gateway. AWS documents that instances behind a public NAT gateway cannot receive unsolicited inbound connections, so a scan of that address describes the gateway. Cancel and scan the address that receives inbound traffic with the tools in the "check it yourself" section.
To keep a copy, request plain output and tee it to a file:
curl -4 -sN "https://qsa.sh/?nocolor=1" | tee "exposure-$(date +%F).txt"
Why the config and the scan disagree
A rule set describes intent. The scan reports which ports completed a TCP connection from an outside network.
The rule exists but is attached to nothing, or a looser rule is still attached
AWS security groups support allow rules only, and when several groups are attached to one resource their rules are aggregated. An old group that opened a port to 0.0.0.0/0 keeps that port open while it stays attached, whatever the new group says. On Azure, a subnet NSG is evaluated first and then the NIC NSG, and when both exist, inbound traffic must be allowed by both. A Hetzner Cloud Firewall applies only to the servers you select or that match its label selector.
A container published a port
A port published with docker run -p can be reachable even when UFW reports it as blocked. Docker published ports skip UFW covers the cause and the fixes.
IPv6 has its own rules
AWS treats 0.0.0.0/0 and ::/0 as separate sources. Each Google Cloud firewall rule holds one address family: IPv4 ranges or IPv6 ranges. On the host, iptables filters IPv4 and ip6tables filters IPv6. The OVHcloud Edge Network Firewall is available for IPv4 addresses only, and the Hetzner Robot firewall filters only IPv4 until you enable its IPv6 filter. A server can be locked down on v4 and open on v6. qsa.sh scans IPv4, so check the v6 address with the commands further down.
A load balancer has its own address
A managed load balancer listens on its own public address with its own rules. In the pattern AWS documents, the load balancer's group allows HTTP and HTTPS from 0.0.0.0/0 and the web servers accept traffic only from the load balancer. A scan run from the instance tests the instance's address. Scan the load balancer separately from another machine.
To see what the server listens on locally, compare the scan with:
sudo ss -tlnp
A socket bound to 0.0.0.0, [::] or * accepts connections on every interface, and the cloud and host firewalls decide whether outside traffic reaches it.
Provider firewall reference
| Provider and layer | Inbound default | Details to check |
|---|---|---|
| AWS security groups | A new group has no inbound rules, so no inbound traffic is allowed | Stateful, allow rules only. Rules from every attached group combine. IPv6 sources need their own rules. |
| Google Cloud VPC firewall rules | Implied deny ingress | Stateful. The default network ships with default-allow-ssh (tcp:22), default-allow-rdp (tcp:3389) and default-allow-icmp from 0.0.0.0/0. A rule holds IPv4 or IPv6 ranges. |
| Azure network security groups |
DenyAllInbound at priority 65500 |
Stateful. Custom rules use priorities 100 to 4096, lowest number first. When subnet and NIC both have an NSG, both must allow. "Effective security rules" and IP flow verify show the combined result. |
| Hetzner Cloud Firewall | With a firewall applied, inbound traffic with no matching rule is dropped | Stateful. Applied per server or by label selector. The FAQ says it does not yet filter private networks or load balancers. |
| Hetzner Robot firewall (dedicated) | Enabled with no rules, it blocks all incoming traffic | Stateless, set on the switch port, 10 rules per direction. IPv6 filtering is off until you enable it. |
| OVHcloud Edge Network Firewall | Accept-only rule sets have no effect; a deny rule is required | Stateless, IPv4 only, configured per IP, up to 20 rules. Filters traffic from outside the OVHcloud network. OVHcloud advises a local firewall as well. |
Reading the three result sections
Open ports (naabu)
Each line is a TCP port that accepted a connection from the scanner. Mark every port you did not intend to publish. The free scan covers the top 1,000 TCP ports, so a service on a port outside that list will not appear.
Service versions and CVE matches (nmap and vulners)
qsa.sh runs nmap -sV --script vulners on every open port. Nmap probes the port to identify the service and version, and the vulners script sends that software name and version to the vulners.com API and prints the known CVE IDs with their CVSS scores. Confirm it is the service you meant to run. The match is based on the reported version string, so confirm each CVE against your distribution's security tracker before you plan a patch.
Nuclei findings
The free scan runs about 2,000 curated nuclei templates: exposed files such as .env and .git, default logins, takeovers, TLS issues, and KEV and critical or high CVEs matched to the detected services. It prints a severity breakdown and the top 3 findings in full.
What the free scan covers and what Full and Deep add
| Free | Full ($5/month) | Deep ($7 one-time) | |
|---|---|---|---|
| Ports | Top 1,000 TCP | All 65,535 TCP | All 65,535 TCP |
| Nuclei checks | About 2,000 curated | About 2,000, across all ports | Full set of about 10,500 plus custom templates |
| Findings shown | Ports, versions, severity counts, top 3 | Full list plus remediation | Full list plus remediation |
| Delivery and time | Live stream, about 30 seconds | Single-read link, about 2 to 12 minutes | Emailed link, about 13 to 16 minutes |
| Scans per IP | 1 per 24 hours | 1 per hour | No cooldown |
Paid scans run with curl https://qsa.sh/<token> from the server you want scanned, and they scan the connecting IP the same way. Free results are never stored. A paid result is held in Redis until it is first opened, then deleted, and purged after 24 hours if nobody opens it. Details are on the pricing and privacy pages.
qsa.sh does not schedule scans or send alerts. To re-scan on a timer and get notified when the result changes, use the script in Monitor server exposure with cron.
Check it yourself from another machine
qsa.sh runs naabu 2.7.0, nmap 7.993 with the vulners script, and nuclei 3.11.1 with the public nuclei-templates. All three are open source and you can run them yourself. Use a machine outside the server's cloud network and scan only addresses you are authorized to test. This also covers load balancers and IPv6 addresses.
Install the tools. naabu and nuclei build with Go, naabu needs libpcap, and the vulners script ships with nmap:
sudo apt install -y nmap libpcap-dev
go install -v github.com/projectdiscovery/naabu/v2/cmd/naabu@latest
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
nuclei -update-templates
Scan the server's public IPv4 address in the same three stages:
TARGET=203.0.113.10
naabu -host "$TARGET" -top-ports 1000 -silent -o ports.txt
nmap -Pn -sV --script vulners -p "$(cut -d: -f2 ports.txt | paste -sd, -)" "$TARGET"
nuclei -l ports.txt -s critical,high,medium
Replace -top-ports 1000 with -p - to sweep all 65,535 ports. naabu runs a SYN scan with root privileges and a CONNECT scan without them.
For IPv6, find the server's global address on the server, then scan it from a machine with IPv6 connectivity:
ip -6 addr show scope global
nmap -6 -Pn -sV --top-ports 1000 2001:db8::10
What to do with common findings
Decide first whether the port should be public. If it should not, close it at the cloud firewall, close it at the host firewall, and bind the service to 127.0.0.1 or the private interface, so the other layers still block it if one is misconfigured.
-
SSH open to
0.0.0.0/0. Limit the source to your office or VPN range in the cloud rule. On Google Cloud, check whetherdefault-allow-sshis still in place. Use key authentication. - A database or cache port such as 3306, 5432, 6379 or 27017. Remove the public allow rule, bind the service to a private address, and reach it over the private network, a VPN or an SSH tunnel. If the port belongs to a container, fix the published port mapping.
-
A port that appears only on IPv6. Add the matching IPv6 rule or remove the
::/0allow rule, and checkip6tablesor your firewall's IPv6 setting. - CVE matches on an intended service. Update the package, then confirm the fixed version on your distribution's tracker.
-
Exposed
.envor.gitpaths. Block the path in the web server, remove the files from the document root, and rotate any credentials they contained. - TLS findings. Renew or fix the certificate chain and set protocols and ciphers from the Mozilla SSL Configuration Generator.
After each change, scan again. A port counts as closed once an outside connection to it fails. The Full tier's one-scan-per-hour limit is sized for fixing and re-checking in the same session.
FAQ
Why does qsa.sh refuse my server?
qsa.sh refuses IPv6 callers, known CGNAT and mobile-carrier ranges, and addresses its reputation data flags as a proxy, VPN or Tor exit. Cloud, hosting and datacenter IPs are accepted. On a dual-stack server, run curl -4 https://qsa.sh.
Can I scan my load balancer or a different server with qsa.sh?
No. qsa.sh scans only the IP the request comes from, on every tier. Run the open-source tools from another machine against the load balancer's address.
Does the scan see ports that my firewall allows only from my office IP?
The scan comes from qsa.sh's own scanner network, so it sees what an arbitrary outside host sees. Ports restricted to your office or VPN range stay out of the open-ports list, which is the result you want.
Top comments (0)