charlesreid1.com blog

Fermi Problems Are Quadrature Problems

Posted in Mathematics

permalink

Here's a Fermi problem you'll run into sooner or later, usually in a job interview, sometimes over drinks with someone who wants to see how you think:

How many chocolate bars are eaten each year in the United States?

The provenance goes back to Enrico Fermi's Chicago physics classes, where he'd ask students to estimate the number of piano tuners in the city, or the width of a nail's head in miles, or anything else where the point wasn't the number but the decomposition. That habit of mind has since been laundered through McKinsey and Google into the standard interview format, but the underlying move is Fermi's: break a hard question into smaller questions you can each guess within a factor of two, and let the errors mostly cancel.

Does Size Matter?

Before we start estimating, there's an elephant in the room that deserves to be named and then, deliberately, ignored. What counts as "a chocolate bar"? A fun-size Snickers you get in a Halloween bucket? A king-size Twix? A square of a Hershey's bar? A Costco-sized Toblerone? The answer changes the final count by an arbitrary factor of 1/2 or 2 or worse - it's the single variable this whole problem is most sensitive to, and it's not really a quantitative question at all. It's a linguistic knob. Turning it doesn't teach us anything about populations or quadrature; it just relabels what we're counting.

So we're going to do what physicists do when a problem has a knob like this: we're going to assume a spherical cow. In the spirit of Fermi, "eating a chocolate bar" is an idealized, dimensionless event - a discrete unit of chocolate consumption, roughly the size of whatever the reader pictures when they hear the phrase. We're not going to quibble about grams or servings or whether a Reese's cup counts. If your definition differs from mine by a factor of two, your final answer differs from mine by a factor of two, and that's fine. The interesting structure of this problem lives in the population and its interactions, not in the definition of the counting unit, and the machinery we're about to build works the same way whichever definition you pick.

With that noted and set aside, on to the actual estimation.

The Naive Approach and What It's Secretly Assuming

The textbook approach to the chocolate question looks like this. Guess that the average American eats maybe one chocolate bar every few days - call it 0.3 per day. Multiply by the population, roughly 340 million. Multiply by 365. You get something on the order of 40 billion chocolate bars per year.

That's your answer, and if you've picked reasonable numbers it's probably within a factor of two or three of the truth.

But look at what that multiplication is quietly assuming: one average American, one average day, one average rate. Every source of variation in the real population has been smoothed into a single number. If you push on the estimate, the assumption starts to feel thin. A five-year-old on Halloween is not eating the same amount of chocolate as an fifty-year-old on Halloween. Neither of them is eating the same amount on Halloween as they are in March. And it gets worse when you notice that the seasonal spike itself depends on who you are: kids own Halloween, adults own Valentine's Day, families with young children own Easter. (We can pretty safely assume that seniors will be flat across all of it.)

You can try to refine the estimate by splitting into age buckets, or by splitting into seasonal buckets. Either helps a little. But do them separately and you still miss the thing that actually matters, which is that age and season interact. A "seasonal multiplier" averaged over the whole population says everyone eats three times more chocolate on Halloween. That's false in a specific and important way: kids eat ten times more, adults eat only slightly more, and the average is a fiction that lives in neither group.

The interaction is real, and no amount of separately-refined marginal averages will recover it. We need a formalism that lets us write the joint structure down directly.

Population as a Joint Density (or, A Little Quadrature Never Hurt Anyone)

Here's the reframe. Think of the population as a density over some space of coordinates. Each person has internal coordinates \(\xi\) - age, income, dietary preferences, whatever matters for the problem - and lives in the external coordinate of time \(t\). Let \(n(\xi, t)\) be the number density of people: \(n(\xi, t)\, d\xi\) is how many people have coordinates in a little box around \(\xi\) at time \(t\). Let \(r(\xi, t)\) be the per-capita consumption rate: chocolate bars per person per unit time, for a person with coordinates \(\xi\) at time \(t\).

Then the total number of chocolate bars eaten in a year is just the integral of the product:

$$ \text{CBE} = \int_0^{1\,\text{yr}} \int_\xi r(\xi, t) \, n(\xi, t) \, d\xi \, dt $$

Two objects, cleanly separated. \(n\) says who exists. \(r\) says what they do. The product \(r \cdot n\) is chocolate bars per unit coordinate per unit time, and integrating it over the whole domain gives the total. (This is the population balance equation, a workhorse in chemical engineering, particle dynamics, and demography. We're borrowing the machinery, not reinventing it.)

Now the key observation: any numerical evaluation of that integral is a quadrature. Pick a grid of abscissae \((\xi_i, t_j)\), assign each grid cell a weight \(w_{ij}\) (in the simplest case, just the cell's width), and sum:

$$ \text{CBE} \approx \sum_{i,j} w_{ij} \, r(\xi_i, t_j) \, n(\xi_i, t_j) $$

That's the whole game. The naive Fermi estimate we started with - rate × population × 365 - is exactly this sum with a single term: one \(\xi\)-bin covering everyone, one \(t\)-bin covering the whole year, one average rate. It's a one-point quadrature over the joint density. Refining a Fermi estimate is refining a quadrature grid. Everything from the back-of-envelope guess up to a full numerical integration lives on the same continuum.

And crucially, this formulation does not require \(r\) to factor as an age-dependent piece times a seasonal piece. You fill in each grid cell independently, so any interaction between coordinates is preserved by construction. That's exactly the structure the marginal-average approach destroys.

There's one more idea worth borrowing from numerical analysis here. The whole art of Gaussian quadrature is that abscissae should not be evenly spaced - they should cluster where the integrand varies fastest. The same instinct applies to Fermi estimation: a good quadrature grid matches the shape of what you're integrating, not the shape of the axis you're integrating over. For the chocolate problem, the "time" axis has twelve months, but the integrand has three spikes: Halloween, Valentine's, Easter. Using twelve evenly-spaced monthly bins would spend most of your resolution on quiet months where the integrand is flat and boring. Concentrating your grid on the three candy-heavy windows plus one bin for "everything else" captures almost all the structure with a quarter of the work. That reduction - twelve bins to four - is not a shortcut; it's the correct grid for this integrand.

There's a subtlety worth naming here, though. When we collapse twelve evenly-spaced months into four unevenly-sized "seasons," we're doing something a little sneaky: we're letting the content of the year (which holiday, which age group's spike) reshape the time axis itself. What was a pure external dimension - clock time, marching uniformly forward - is being warped into an internal dimension that carries information about the integrand. That works cleanly here because there's no other process on the time axis we care about. As problems gain dimensions, though, that alignment can break.

(As a more involved example, if we were trying to approximate number of chocolate bars eaten by a population, but under conditions of continuous changes in population or demographics, then integrating the total population over time would need a properly-resolved time axis - lumping 46 weeks into one bin would be throwing away real information. The seasonal grid works for our problem, because the integrand's structure and external axis's structure happen to coincide. If they don't, keep them separate, and "pay" for the extra abscissas (with a bit more bookkeeping).

Working the Chocolate Problem With Interactions

With the framework in hand, the estimate becomes a table. Rows are age buckets, columns are seasonal windows, each cell holds the number of bars eaten by that group in that window.

For the age axis, four buckets are enough: kids (0-12), teens (13-19), adults (20-64), and seniors (65+). Population is roughly uniform over these bands, so with 340 million Americans we can allocate roughly 55M kids, 30M teens, 190M adults, and 65M seniors. (Uniform-in-age is a lie, but a small enough one that the drama of this problem lives in \(r\), not in \(n\). That itself is a useful diagnostic - it tells us where refining would and wouldn't help.)

For the time axis, four bins: a Halloween window (~2 weeks around Oct 31), a Valentine's window (~2 weeks around Feb 14), an Easter window (~2 weeks around the spring holiday), and the remaining ~46 weeks of the year lumped into "rest of year." Numbers below are bars per person per day, and we'll multiply through by bucket population and bin length at the end.

Halloween (14d) Valentine's (14d) Easter (14d) Rest (312d)
Kids (55M) 3.0 0.4 1.5 0.3
Teens (30M) 2.0 0.6 0.4 0.4
Adults (190M) 0.5 1.5 0.3 0.25
Seniors (65M) 0.2 0.3 0.2 0.15

Look at that matrix for a second. The kids row peaks on Halloween. The adults row peaks on Valentine's. Easter has a bump for kids and almost nothing for anyone else. Seniors are flat and low. No product of a row-vector and a column-vector produces this pattern - the matrix is not rank-1, and any factorization \(r(\xi, t) = r_{\text{age}}(\xi) \cdot s(t)\) would flatten these ridges into a smooth surface that gets every cell wrong. This is precisely the structure the joint formulation preserves and the marginal-refinement approach loses.

Summing (population × rate × days) cell by cell:

$$ \text{CBE} \approx \sum_{i,j} n_i \cdot r_{ij} \cdot w_j \approx 44 \text{ billion bars/year} $$

Which is, satisfyingly, in the same order of magnitude as the naive estimate from the first section (40 billion for the naive approach, 44 billion for the quadrature estimate). That's the usual outcome, and it's not a knock on the framework - it's a reminder that averaging over correlated variables often lands close to the truth by luck. What the framework buys you isn't necessarily a better number; it's a legible number. Every approximation is named and located in a specific cell. If you wanted to sharpen the estimate, you'd know exactly where to add resolution - split Halloween into "trick-or-treat night" and "the week after," split kids into "young enough to trick-or-treat" and "too cool for it" - and you'd be adding grid points to the ridges, which is exactly what a higher-order quadrature scheme does automatically.

That's the transferable idea, and it generalizes beyond chocolate. Any Fermi problem about a population - how many haircuts per year, how many gallons of coffee consumed per week, how many miles driven per day - is an integral of a per-capita rate against a number density over some joint coordinate space. The reason the standard "multiply the averages" trick works at all is that most people's mental \(r\) is smooth enough that a one-point quadrature suffices. When it isn't - when the coordinates interact, when the ridges matter - the framework tells you exactly where to add resolution and why. You stop guessing a single average and start choosing a grid.

References

Tags:    mathematics    fermi problems    quadrature    numerical methods    chocolate   

Nmap Host Discovery: All the Ways to Ask "Is Anyone There?"

Posted in Security

permalink

This is a companion post to Building an Nmap Short Course from Scratch. Where that post was about the meta - course design, lab infrastructure - this one drills into the actual first-lecture material: how Nmap decides whether a host is up.

Full lecture notes: Nmap/Short Course/Lecture 1.

Why Host Discovery Matters

Before you can scan ports, identify services, or check for vulnerabilities, you have to figure out which IP addresses on the target network actually have a machine behind them. Scanning IPs that aren't responding is a waste of time, generates a lot of unnecessary network noise, and can tip off defenders.

Think of it as making a map of active settlements before deciding which ones to explore in detail.

Ethics First

The obvious but necessary caveat: Nmap must only be used on networks where you have explicit, written authorization to scan. Unauthorized scanning can be interpreted as an attack, and in many jurisdictions is illegal.

Everything in this post assumes an isolated lab environment - which is exactly what we set up in the previous post.

The Default: Nmap's Multi-Probe Approach

If you run Nmap as a privileged user (root or sudo) without specifying any discovery options, it will fire four probes at each target:

  1. ICMP echo request (a classic ping)
  2. TCP SYN packet to port 443
  3. TCP ACK packet to port 80
  4. ICMP timestamp request

If any of the four gets a response, Nmap considers the host up.

The multi-probe approach exists because different firewalls block different things. ICMP is commonly blocked at the network edge. Port 443 might be allowed inbound because there is a web server behind it. Port 80 might respond with a TCP RST because there is nothing listening. Any one of these signals is enough.

Unprivileged users can't send raw packets, so Nmap falls back to attempting TCP connect() calls to ports 80 and 443. Less accurate, but works without root.

-sn: Just Tell Me What's Alive

Nmap's default behavior is to do host discovery and then port scan whatever comes back alive. If you only want the host discovery step, use -sn:

nmap -sn 192.168.1.0/24

-sn means "scan, no port scan." (In older versions it was -sP, "scan ping.") It runs the multi-probe discovery and prints just the list of live hosts. Very fast, very quiet compared to a full port scan, and often the first thing you run.

Everything else in this post uses -sn unless otherwise noted.

The -P Family: Picking a Specific Probe

If you want to control exactly which probe Nmap sends, use one of the -P flags.

-PE: ICMP Echo (the plain ping)

sudo nmap -sn -PE 192.168.1.100

The most familiar probe. Sends an ICMP echo request, expects an ICMP echo reply. Works when firewalls allow ICMP. Frequently blocked at network perimeters.

-PP: ICMP Timestamp

sudo nmap -sn -PP 192.168.1.101

Sends an ICMP timestamp request (type 13), expects a timestamp reply (type 14). Useful when echo requests are blocked but timestamp requests aren't - some firewall rules block ICMP type 8 (echo) but forget about type 13.

-PM: ICMP Address Mask

sudo nmap -sn -PM 192.168.1.102

Sends an ICMP address mask request. Very rarely used legitimately these days, which is exactly why it sometimes gets through firewalls that block the more common ICMP types.

The pattern: try the obvious probe, fall back to the less-obvious ones if the obvious one fails.

-PS[ports]: TCP SYN Ping

sudo nmap -sn -PS 192.168.1.0/24
sudo nmap -sn -PS22,80,443 192.168.1.50

Sends a TCP SYN packet to the given ports (default 80 if you don't specify). A response - either SYN/ACK meaning "port open" or RST meaning "port closed" - tells Nmap the host is alive.

This one is the workhorse against firewalled targets. Firewalls typically allow inbound traffic to common service ports (80, 443, 22) because there are legitimate reasons for outsiders to reach those ports. TCP SYN pings ride on that permitted traffic.

-PA[ports]: TCP ACK Ping

sudo nmap -sn -PA 192.168.1.0/24
sudo nmap -sn -PA21 192.168.1.55

Sends a TCP packet with the ACK flag set. This is a weird packet - an ACK with no prior SYN - so most operating systems respond with a TCP RST regardless of whether the port is open. If you see the RST, the host is alive.

Useful against stateful firewalls that block unsolicited SYN packets (because they aren't part of any tracked connection) but let ACKs through (because ACKs look like the middle of an established connection the firewall might have lost track of).

-PU[ports]: UDP Ping

sudo nmap -sn -PU 192.168.1.0/24
sudo nmap -sn -PU53,161 192.168.1.60

Sends a UDP packet to the given ports (default 40125, chosen because it's usually closed). If the port is closed, the host should respond with ICMP port unreachable, telling you it's alive. If the port is open, you might not get a response at all.

UDP ping is less reliable for host discovery on its own, but very useful when the target runs UDP services (DNS on 53, SNMP on 161) and when other probes are all blocked.

-PR: ARP Ping

sudo nmap -sn -PR 192.168.1.0/24

The gold standard when you are on the same Ethernet segment as your targets. ARP is the layer-2 protocol that resolves IP addresses to MAC addresses, and hosts cannot refuse to answer ARP - if they did, they would be unable to talk to anything on the local network.

Nmap automatically uses -PR for local-segment targets when run by a privileged user, unless you tell it not to (--send-ip). It's fast (no round trip past the switch) and 100% reliable for hosts that are up.

Target Specification

Independent of the probe type, you need to tell Nmap which IPs to scan.

Single addresses

nmap 192.168.1.1
nmap scanme.nmap.org

Hostnames get resolved via DNS. scanme.nmap.org is a target the Nmap project maintains specifically for people to practice against.

CIDR ranges

nmap -sn 192.168.1.0/24
nmap -sn 10.0.0.0/8

Standard CIDR notation. /24 is 256 IPs, /8 is 16.7 million (be careful).

Numeric ranges and lists

nmap -sn 192.168.1.1-100        # .1 through .100
nmap -sn 192.168.1.1,2,10,50    # specific IPs
nmap -sn 192.168.1,2,3.1-254    # cross product

That last one scans .1-.254 for each of 192.168.1.x, 192.168.2.x, and 192.168.3.x. Useful for scanning a handful of adjacent subnets.

From a file

nmap -sn -iL targets.txt

One target per line in the file. Best option when you have a large or irregular list of targets, or when the target list comes from another tool.

Excluding targets

nmap -sn 192.168.1.0/24 --exclude 192.168.1.1,192.168.1.100
nmap -sn 192.168.1.0/24 --exclude-file dontscan.txt

Critical for avoiding accidental scans on the CEO's laptop or the production database when you're supposed to be scanning a specific subnet.

Timing: -T0 through -T5

Nmap has six timing templates that control how aggressively it sends packets:

  • -T0 (paranoid): Extremely slow. One probe every few minutes. Used for IDS evasion in serious red-team engagements.
  • -T1 (sneaky): Slow. IDS evasion, but not as extreme.
  • -T2 (polite): Slower than default. Reduces bandwidth and target load. Good for scanning production infrastructure.
  • -T3 (normal): The default. Reasonable timing for most networks.
  • -T4 (aggressive): Faster. Assumes a reliable network. Good balance for lab work.
  • -T5 (insane): Very fast. Sacrifices accuracy for speed. Can overwhelm slow networks or fragile targets.
sudo nmap -sn -T4 192.168.1.0/24

For host discovery specifically, -T4 is usually the right choice in a lab environment. On production, use -T3 or -T2 and be patient.

Reading the Output

A successful scan looks like this:

Starting Nmap 7.94 ( https://nmap.org ) at 2025-05-27 14:00 PDT
Host 192.168.1.1 is up (0.00050s latency).
MAC Address: AA:BB:CC:DD:EE:FF (Realtek Semiconductor)
Host 192.168.1.10 is up (0.00080s latency).
MAC Address: 11:22:33:44:55:66 (VMware)
Nmap done: 256 IP addresses (2 hosts up) scanned in 2.10 seconds

The MAC address only shows up when you're on the same Ethernet segment. The vendor in parentheses comes from Nmap's built-in OUI database - useful for spotting VMs, or figuring out which switch port a device is behind.

When Hosts Don't Appear

"Host is down" in Nmap's output really means "Nmap didn't get a response from any of the probes it sent." That's not the same as "the host is offline." Common reasons a live host doesn't appear:

  • Restrictive firewall. The most common culprit. Dropping all probe types silently is a valid (if aggressive) defensive posture.
  • Host-based firewall. Windows Firewall, iptables, and similar can block probes even when the network firewall is permissive.
  • Wrong scan for the environment. ICMP-only discovery against a target that only accepts TCP-80 - the host won't appear even though a -PS80 scan would find it in an instant.
  • Unprivileged Nmap. Non-root Nmap has very limited discovery options. Always run as root (or via sudo) for real scanning.
  • Network layer issues. Routing problems, wrong subnet mask on the scanner, VLAN mismatches - anything that prevents packets from reaching the target.

The right response to a "no hosts up" result on a network you know is alive: try a different discovery method. If ICMP fails, try -PS22,80,443. If TCP fails, try -PA. If nothing works, you're probably up against a very well-configured firewall.

From Discovery to Deeper Scans

Once you have a list of live hosts, the natural next step is port scanning them to see what services are running. The clean way to chain this is with grepable output:

nmap -sn -oG - 192.168.1.0/24 | awk '/Up$/{print $2}' > live_hosts.txt
nmap -sV -iL live_hosts.txt

The first command produces a machine-parseable list of live IPs. The second feeds that list into a service-detection scan. This two-phase approach is efficient (you don't waste time port-scanning dead IPs) and organized (you have a saved list of live hosts to work from later).

Service detection, port scanning, NSE scripting - all of that comes in later lectures of the course. But it all starts with knowing who's home.

References

Tags:    security    nmap    host discovery    ping    arp    networking    pentesting   

Building an Nmap Short Course from Scratch

Posted in Security

permalink

We spent a good chunk of late May 2025 building a short course on Nmap from scratch - 12 lectures, 12 companion labs, plus the entire virtual lab infrastructure the students would use to run the labs. The whole thing lives on our wiki under Nmap/Short Course.

This post is not about the Nmap material itself (that comes in the next post). It is about the design decisions behind the course - why 12 lectures, why a fully isolated cloud lab, why Vagrant + Docker + Ansible instead of picking one, and what we would do differently if we started over.

Course Shape

The course is organized into three modules:

  • Module 1: Nmap Mastery - Beyond the Basics. The core Nmap material: host discovery, port scanning, service and OS detection, the scripting engine (NSE), and output formats.
  • Module 2: Red Team Nmap - Offensive Recon & Vuln Identification. Using Nmap for reconnaissance in an authorized engagement: fingerprinting, vulnerability enumeration via NSE, and integrating results into a broader recon workflow.
  • Module 3: Blue Team Nmap - Auditing, Defense & Network Monitoring. The other side: using Nmap for asset inventory, compliance checks, detecting unauthorized services, and pairing Nmap output with IDS rules.

Twelve lectures split across those three modules. Every lecture has a companion lab. The labs share a single virtual environment that gets richer over the course - by the time students are in Module 3 they are scanning the same infrastructure they attacked in Module 2.

Why Twelve Lectures

We picked 12 because it maps cleanly to a compressed summer session (a lecture + lab per week for a 12-week course, or two per week for a 6-week intensive). It also gave us enough room to introduce Nmap options in the order they build on each other, without cramming.

Twelve is a round number, each lecture is a coherent unit, and the whole thing still fits in a summer course.

The Lab Environment: The Big Boy

The lab infrastructure is the part we spent the most time on. Our requirements:

  • Students should be able to scan without touching any network they don't have explicit permission to scan
  • The lab should have a variety of realistic services and vulnerabilities, not just one target
  • Adding a new lab scenario should be a few lines of code, not a reinstall
  • The instructor should be able to reset the whole environment to a known-good state before every class

The design we landed on:

A single large EC2 instance as the lab host. We call it "the big boy" in our notes. Something like an m5.xlarge if the budget allows. Ubuntu Server LTS on the host, because Vagrant/libvirt/KVM has the smoothest experience there. Storage is EBS gp3, 80-100 GB.

Nested virtualization on that host. The EC2 instance runs KVM (via libvirt), which runs full VMs for the more heavyweight targets (and one attacker VM per student), plus Docker for lightweight containerized services. Vagrant orchestrates the VMs, Docker Compose orchestrates the containers, and both live on the same private virtual network (192.168.50.0/24 in our example).

Ansible for configuration management. Every target service, every firewall rule, every open port is defined in an Ansible playbook. Changing the lab is editing YAML, not clicking around in Docker or SSHing into VMs and running commands.

Students SSH into an attacker VM. They do not SSH into the EC2 host directly, and they do not connect via VPN. Each student (or shared pair of students) gets a preconfigured attacker VM with Nmap and the other course tools installed. That VM sits inside the lab network and can reach all the targets.

The recommendation we did not take: a VPN-based approach where students connect to the EC2 host and run Nmap from their own laptops. We ruled it out because it puts environment consistency on the student's shoulders - their local Nmap version, their local firewall, their local OS. The attacker-VM approach guarantees everyone is running the same tool from the same place.

Why This Stack

The "why Vagrant + Docker + Ansible instead of picking one" question comes up a lot. Short version:

  • Vagrant is the right tool for full VMs that need to look and behave like real hosts (a Windows target, an outdated Linux with a vulnerable SSH). Vagrant plays well with libvirt/KVM on Linux.
  • Docker Compose is the right tool for lightweight service targets: a vulnerable web app, an FTP server, a Samba share. One container, one service, one IP.
  • Ansible is the right tool for configuration - install this software, open this port, run this service - and it works identically on Vagrant VMs and Docker containers.

Each tool does waht it is best at. Trying to make Docker do full-VM work is possible but painful. Trying to make Vagrant manage 30 tiny services is possible but slow. Ansible glues them together with the same configuration language.

Cost Model

The EC2 instance is the main cost driver. Some things we do to keep it reasonable:

  • Stop the instance when not in use. Evenings, weekends, and between class sessions. We only pay for the EBS storage during those times, not the compute.
  • Start small on the instance size. If it is not enough, we upgrade.
  • Use Elastic IP or a cheap domain. Use an easy to remember domain like nmap-lab.our-course.net.

Spot Instances would save a lot more, but we ruled them out because they can be terminated with little notice, and getting evicted 15 minutes into a lab session is not the experience we want to give students.

Notes for the Instructor

A few things we would tell anyone building a course like this from scratch:

Version everything. The whole lab is a Git repo: Vagrantfile, docker-compose.yml, Ansible playbooks, and the Ansible inventory. Every branch is a different lab scenario. Rolling back is git checkout.

Test the lab reset every time. Before every session, tear the whole environment down (docker-compose down -v && vagrant destroy -f) and bring it back up from scratch. This catches "works on my machine" bugs.

Write the lab handout after you build the lab. The lab handout is the source of truth for what students see and do. If you write the handout first and then build the lab to match it, you will end up writing two things that don't quite line up.

Instrument the attacker VM. We put a shell history file that survives resets, plus a script that logs every Nmap command run during the session, so students can review what they did. Also useful when a student says "I typed the command exactly and it didn't work."

What This Cost Us

The course took roughly two solid weeks to build end-to-end - one week on the lab infrastructure, one week on the lecture content and lab exercises.

expected to run more than once. If we were doing it as a one-off, we would have skipped the Ansible layer and hard-coded the target configurations, saving maybe two or three days at the cost of a much worse experience if we ever wanted to change anything.

If you are considering doing this: yes, do it. The single biggest teaching lever is being able to show students a live network with real services responding to their scans, and this setup does that without ever putting them or you in legal jeopardy.

References

Tags:    security    nmap    teaching    curriculum    aws    vagrant    docker    ansible    pentesting   

March 2022

How to Read Ulysses

July 2020

Applied Gitflow

September 2019

Mocking AWS in Unit Tests

November 2017

A Hard(y) Math Problem