ELBaph - AWS Elastic Load Balancer Configuration Auditor

A whitebox CLI tool for pentesters and security engineers to enumerate AWS Application Load Balancers and detect common routing misconfigurations that expose internal services to the public internet.
Built by Doyensec as part of CloudSec Tidbits Season 2.
Check reference (behaviour, samples): docs/elbaph-checks-documentation.md.
Purpose
ELBaph is an active/passive auditing framework written in Go. Its goal is to identify routing misconfigurations and logical vulnerabilities across AWS Elastic Load Balancers (ELBs) that typical security posture management tools (CSPMs) miss.
Instead of just checking if logging is enabled or if a WAF is attached, ELBaph:
- Evaluates listener conditions (paths, host headers, source IPs).
- Cross-references Target Groups and their EC2 instance members across different ALBs.
- Analyzes interactions with surrounding network components (VPCs, CloudFront distributions).
- Performs targeted HTTP reachability probes to validate if backend services are truly exposed.
Installation
git clone https://github.com/doyensec/elbaph.git
cd elbaph
go build -o elbaph .
Requires Go 1.24 or later.
Usage
# Scan a region - findings printed live, output folder created automatically
elbaph scan --region us-east-1
# Scan multiple regions using an AWS profile
elbaph scan --all-regions -p my-pentest-profile
# Inline access key + secret (optional session token for STS creds)
elbaph scan -r us-east-1 --aws-access-key-id AKIAEXAMPLE --aws-secret-access-key '...'
# Shared credentials file (INI, same layout as ~/.aws/credentials) and profile name
elbaph scan -r us-east-1 --aws-credentials-file ./readonly.creds -p customer-audit
# Route active HTTP probes through a remote proxy.
# Useful when running the tool from an internal network or corporate VPN
# to mimic an external vantage point and avoid false-positive reachability results.
elbaph scan -r us-east-1 --proxy http://remote.vantage.point:8080
# Just output everything to a generic "out" folder as JSON and disable terminal colors
elbaph scan -r eu-west-1 -d out -o json --no-color
# Run only specific checks
elbaph scan --region us-east-1 --checks alb-external-reachability,alb-ip-gate-bypass
# Also export a structured report (alongside the always-created text folder)
elbaph scan --region us-east-1 --output json
elbaph scan --region us-east-1 --output markdown
elbaph scan --region us-east-1 --output sarif
# Write the structured export to a specific path
elbaph scan --region us-east-1 --output sarif --file /tmp/results.sarif
# Suppress terminal colours (useful in CI)
elbaph scan --region us-east-1 --no-color
Flags
| Flag |
Default |
Description |
-r, --region |
us-east-1 |
AWS region to scan (ignored as sole region when --all-regions is set) |
--all-regions |
- |
Scan every region enabled for the account (ec2:DescribeRegions) |
-p, --profile |
- |
AWS named profile (with a custom credentials file, selects the profile block; otherwise uses shared config as usual) |
--aws-access-key-id |
- |
Access key; must be used with --aws-secret-access-key; mutually exclusive with --aws-credentials-file |
--aws-secret-access-key |
- |
Secret key; must be used with --aws-access-key-id |
--aws-session-token |
- |
Optional session token when using temporary (STS) inline credentials |
--aws-credentials-file |
- |
Path to a credentials INI file (same format as ~/.aws/credentials); mutually exclusive with the inline key flags; combine with -p for a non-default profile |
-c, --checks |
all |
Comma-separated list of check IDs to run |
-d, --dir |
elbaph-<timestamp> |
Output directory name |
-o, --output |
- |
Also export: json | markdown | sarif |
-f, --file |
<dir>/report.<ext> |
Path for the structured export |
--proxy |
- |
Optional HTTP proxy URL for active probing checks |
--vhost-probe-hosts-file |
- |
For alb-vhost-probe: newline-separated hostnames merged after listener rules and Route 53 alias names but before the Route 53 zone corpus, so your list is not cut off when the corpus hits the candidate cap (Source: file-corpus) |
--vhost-probe-path |
/ |
For alb-vhost-probe: URL path for every GET probe (must start with /, e.g. /api/health) |
--no-color |
- |
Disable terminal colour |
AWS credentials
By default ELBaph uses the AWS SDK default credential chain for the scan region (and us-east-1 for global calls such as CloudFront and DescribeRegions when --all-regions is set): environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN), shared files under ~/.aws/, and the EC2/instance profile when applicable. Use -p / --profile to force a named profile from the shared config.
Inline keys: --aws-access-key-id and --aws-secret-access-key together install a static credential provider for the whole run. Add --aws-session-token when using STS temporary credentials. This mode is mutually exclusive with --aws-credentials-file. Inline secrets are visible in process listings and shell history; prefer env vars, a credentials file, or an IAM role when possible.
Credentials file: --aws-credentials-file points at an INI file in the same format as ~/.aws/credentials ([profile-name] sections and aws_access_key_id / aws_secret_access_key / optional aws_session_token). Without -p, the SDK uses the default profile from that file; with -p myprofile, the [myprofile] section is used.
Proxied Probing
When ELBaph executes active probing checks (such as alb-external-reachability), it performs HTTP connections directly to backend target IPs to determine if they are exposed to the public internet.
Note: Active probing is still under active development. Depending on network topology, proxy behavior, and target protocol specifics, results may be incomplete or inaccurate. Treat probe findings as strong signals that should be validated manually
If you are running ELBaph from within a corporate VPC or behind an enterprise VPN, your machine may possess privileged network access to these targets. This internal routing can result in ELBaph falsely classifying an isolated instance as "reachable from the internet".
To mitigate these false positives, use the --proxy flag to route the active probes through an external vantage point (e.g., an internet-facing proxy server). This ensures that reachability tests accurately reflect the perspective of an external, unauthenticated attacker.
For alb-vhost-probe on HTTP listeners, an http:// proxy (including Burp on 127.0.0.1:8080) is implemented with CONNECT to alb-dns:port, then a normal GET to --vhost-probe-path (default /) whose Host header is the vhost candidate. That avoids a Go net/http quirk where a plain proxied request would otherwise send GET http://<Host>/β¦ and the proxy would contact the vhost name instead of the load balancer. Ensure your proxy allows CONNECT to port 80 when testing HTTP listeners (Burp: Proxy settings β Request listeners β allow β¦ or add an upstream rule as needed). With more than 50 candidate hosts, vhost probes run concurrently; cap parallelism with ELBAPH_VHOST_PROBE_MAX_CONCURRENCY (default 8).
Output
Every scan creates a timestamped folder in the current working directory:
elbaph-2026-04-01_22-00-00/
βββ summary.txt run metadata + per-check finding counts
βββ albs.txt table of all discovered ALBs
βββ nlbs.txt table of all discovered NLBs (scheme, DNS, listeners)
βββ target_groups.txt table of all target groups + health
βββ rules.txt per-ALB listener rule chain dump
βββ findings.txt one detailed block per finding
βββ vhost_route53_attachments.txt Route 53 FQDNs β ALB (alias/cname) for alb-vhost-probe
βββ vhost_route53_corpus.txt record-owner FQDNs from listed zones, excluding zone apex (probe wordlist source)
βββ topology.html vis-network resource map (VPC β ALB β listener β rule β TG β targets; multi-ALB targets highlighted)
The topology.html file is an interactive diagram of how your ELBs sit in the VPC and how traffic is routed through listeners, rules, target groups, and backend targets.
Findings are also printed live to the terminal as each check completes - you do not have to wait for the full scan to finish.
If --output is specified, a report.json / report.md / report.sarif is written into the same folder.
Required AWS Permissions
ELBaph is read-only. The minimum IAM policy required is:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeLoadBalancerAttributes",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:DescribeRules",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetHealth",
"ec2:DescribeRegions",
"ec2:DescribeInstances",
"route53:ListHostedZones",
"route53:ListResourceRecordSets"
],
"Resource": "*"
}
]
}
Optional Route 53 permissions enrich alb-vhost-probe (FQDN corpus and alias correlation). Details: docs/elbaph-checks-documentation.md.
Checks
Built-in check IDs (for -c / --checks). Methodology, sample output, and how to read each finding: docs/elbaph-checks-documentation.md.
| ID |
Name |
alb-client-ip-spoofable |
Client IP Spoofing via Preserved X-Forwarded-For |
cloudfront-bypass |
CloudFront Origin Direct Access |
alb-desync-monitor |
Desync Mitigation Mode is Monitor |
alb-external-reachability |
Target External Reachability |
alb-ip-gate-bypass |
IP Gate Bypass via Alternate ALB |
alb-optional-auth |
Optional Authentication Allowed |
alb-rule-shadowing |
Restrictive Listener Rule Shadowed by Broader Earlier Rule |
alb-vhost-probe |
ALB virtual host routing probe |
nlb-direct-target-reachability |
NLB Target Directly Reachable |
Adding a New Check
A check is a single Go file in internal/checks/. The only contract is the Check interface:
type Check interface {
ID() string
Name() string
Description() string
Run(ctx context.Context, data *collector.Dataset) ([]model.Finding, error)
}
Step-by-step
1. Create the file
internal/checks/my_check.go
2. Implement the interface
package checks
import (
"context"
"fmt"
"github.com/doyensec/elbaph/internal/collector"
"github.com/doyensec/elbaph/internal/model"
)
/*
alb-my-check
Describe what this check looks for, why it matters, and what the exact
trigger conditions are. This comment is the primary documentation for anyone
reading or reviewing the check.
Trigger condition:
ALB scheme = internet-facing
...
*/
func init() { Register(&MyCheck{}) }
type MyCheck struct{}
func (c *MyCheck) ID() string {
return "alb-my-check"
}
func (c *MyCheck) Name() string {
return "alb-my-check"
}
func (c *MyCheck) Description() string {
return "One-line description of what this check detects."
}
func (c *MyCheck) Run(ctx context.Context, data *collector.Dataset) ([]model.Finding, error) {
var findings []model.Finding
for _, lb := range data.LoadBalancers {
// ... detection logic ...
findings = append(findings, model.Finding{
ID: fmt.Sprintf("%s|%s", c.ID(), lb.ARN),
CheckID: c.ID(),
CheckName: c.Name(),
Region: data.Region,
LoadBalancerARN: lb.ARN,
LoadBalancerName: lb.Name,
LoadBalancerScheme: lb.Scheme,
Detail: `Explain what was found and why it is a problem.`,
Remediation: `Explain what should be done to fix it.`,
})
}
return findings, nil
}
3. That's it.
The init() call registers the check globally. No other file needs to be modified. The new check will appear automatically in --checks list output and run as part of the default scan. Add a short entry (ID, name, narrative, sample output) to docs/elbaph-checks-documentation.md and a one-line row in the README checks table.
What data is available
The collector.Dataset passed to Run() contains everything collected from AWS:
| Field |
Type |
Contents |
LoadBalancers |
[]model.LoadBalancer |
All ALBs with listeners, rules, wired target group pointers, and Route53Names (FQDNs that ALIAS/CNAME to the ALB when Route 53 APIs are permitted) |
TargetGroups |
map[string]*model.TargetGroup |
All TGs keyed by ARN, with health data |
Helper functions in internal/albutil/:
| Function |
Purpose |
albutil.ConditionSummary(conds) |
Compact string representation of rule conditions |
albutil.ActionChainDesc(actions) |
Ordered slice of human-readable action descriptions |
The pkg/netutil package provides IsRFC1918(ip).
Project Structure
elbaph/
βββ docs/
β βββ elbaph-checks-documentation.md detailed check documentation + sample findings output
βββ main.go
βββ cmd/
β βββ root.go cobra root command
β βββ scan.go scan subcommand - orchestrates collect β check β output
β βββ version.go
βββ internal/
β βββ model/ shared domain types (LoadBalancer, Finding, ScanMeta, β¦)
β βββ collector/ AWS API calls β model types (no SDK types leak out)
β βββ checks/ detection modules + shared registry
β βββ output/ always-on text folder + live terminal printer
β βββ export/ optional structured exporters (JSON, Markdown, SARIF)
βββ pkg/
βββ netutil/ RFC1918 and CIDR helpers
π€ Contributing
ELBaph thrives on community contributions. Whether you are a developer, researcher, or bug hunter, your feedback and pull requests help improve the project for everyone. Please use the GitHub issue tracker for reports and discussion.
This project is developed with support from Doyensec.
