Deepen PracticeOrdered learning track

Network Boundary Security and Safe Egress

Learn Java Networking - Part 026

Network boundary security and safe egress for Java applications, covering SSRF prevention, URL canonicalization, allowlists, DNS rebinding, private IP filtering, redirect safety, proxy boundaries, and production-grade outbound client design.

18 min read3468 words
PrevNext
Lesson 2632 lesson track1927 Deepen Practice
#java#networking#security#ssrf+5 more

Part 026 — Network Boundary Security and Safe Egress

Core thesis: an outbound network call is not a harmless utility function. It is a capability to make your server reach something. If the destination is influenced by untrusted input, it becomes a security boundary.

This part focuses on networking-specific security for Java applications. It does not repeat general cryptography, authentication, or application-security material from earlier series. The narrow focus here is safe outbound connectivity, especially SSRF-class risk, DNS rebinding, redirect abuse, proxy escape, metadata endpoint exposure, and internal-network access.

A production Java service should not let arbitrary input become arbitrary egress.

The goal is not “sanitize the URL”. The goal is:

Only explicitly authorized network destinations are reachable, under stable identity, with predictable scheme, host, port, DNS, redirects, and proxy behavior.


1. Kaufman Skill Map

1.1 Target capability

After this part, you should be able to design Java networking code that:

  • treats outbound calls as privileged actions;
  • rejects unsafe URL schemes, hosts, ports, redirects, and IP ranges;
  • distinguishes parsing, canonicalization, resolution, authorization, and connection;
  • understands DNS rebinding and time-of-check/time-of-use risk;
  • avoids URL parsing and equality traps in security decisions;
  • validates every redirect hop;
  • uses allowlists by business capability, not raw user-controlled URLs;
  • integrates app-level controls with egress proxy/firewall/service-mesh policy;
  • produces audit logs that explain why an outbound destination was allowed or denied.

1.2 Subskills

SubskillWhy it mattersPractice target
Threat model egressOutbound calls can reach internal assetsDraw what the server can reach that the user cannot
Canonicalize URIAttackers exploit parser ambiguityParse once and validate normalized components
Enforce allowlistDeny-by-default is safer than blocklistMap operation to approved destination templates
Resolve defensivelyDNS can change over timeValidate all resolved addresses and revalidate redirects
Classify addressesPrivate, loopback, link-local, metadata endpoints matterBlock unsafe CIDRs and special addresses
Control redirectsRedirects move destination after approvalDisable auto-redirect or manually validate each hop
Use egress proxyApp validation is not enoughRoute outbound traffic through enforcement point
Audit decisionsSecurity controls need explainabilityLog policy id, normalized target, decision, reason

2. Threat Model: What Are We Defending Against?

The classic risk is Server-Side Request Forgery: an attacker causes your server to send a request to a destination chosen or influenced by the attacker.

This matters because your server often has access that the attacker does not:

  • private VPC services;
  • Kubernetes service DNS;
  • cloud metadata endpoints;
  • localhost admin ports;
  • Redis, Elasticsearch, Prometheus, admin dashboards;
  • internal HTTP APIs;
  • partner APIs with network allowlisting;
  • credentials attached by the server;
  • mTLS identity from the workload;
  • egress NAT IP trusted by other systems.

2.1 Typical vulnerable feature shapes

FeatureRisky input
“Import from URL”user supplies arbitrary URL
Webhook testeruser controls callback target
Image fetcheruser controls image URL
PDF generatorHTML references remote resources
Link previewservice fetches URL to extract metadata
File scannerservice pulls remote file
Federation clientremote server provides next-hop location
OAuth/OIDC metadata fetchissuer/discovery URL misconfigured or attacker-controlled

2.2 What attackers try to reach

TargetExample patternWhy dangerous
Localhost127.0.0.1, [::1], localhostAdmin ports bound locally
Link-local169.254.169.254, fe80::/10Cloud metadata or local network services
Private IPv410/8, 172.16/12, 192.168/16Internal services
Unique-local IPv6fc00::/7Internal IPv6 services
Kubernetes DNS*.svc, service namesCluster-internal APIs
Unix socket bridgelocal proxy exposed on TCPcontainer runtime/admin APIs
Internal proxyopen proxy or CONNECT abusepivot point

3. Security Invariants

Use these as architecture review rules.

3.1 Egress invariants

  1. User input must not directly become a network destination.
  2. The default decision is deny.
  3. Allowed destinations are tied to a business capability.
  4. Scheme, host, port, and path constraints are explicit.
  5. Unsafe IP ranges are blocked even if DNS name looks harmless.
  6. Redirects are disabled or revalidated at every hop.
  7. DNS resolution is not trusted as a one-time permanent fact.
  8. App-level controls are backed by network-level controls.
  9. Credentials are attached only after destination approval.
  10. The decision is auditable.

3.2 Parsing invariants

  1. Parse with URI, not URL, for security decisions.
  2. Reject malformed, opaque, relative, or ambiguous URIs.
  3. Reject userinfo in authority.
  4. Normalize host casing and IDN representation.
  5. Enforce scheme allowlist.
  6. Enforce port allowlist.
  7. Reject fragments for outbound fetch policy.
  8. Reconstruct canonical target from parsed components rather than trusting raw string.

3.3 DNS/address invariants

  1. Resolve host only after syntactic validation.
  2. Validate all resolved addresses.
  3. Reject if any resolved address is unsafe unless policy explicitly allows it.
  4. Revalidate after redirects.
  5. Consider DNS rebinding and TOCTOU between validation and connection.
  6. Prefer a controlled egress proxy for strict enforcement.

4. URL vs URI in Java

Java has both java.net.URL and java.net.URI. For security decisions, prefer URI.

Why:

  • URI is primarily syntactic and does not imply opening a connection;
  • URL represents a locator with protocol handlers;
  • URL.equals and URL.hashCode historically may involve host name resolution, which is inappropriate for parsing, maps, and security decisions;
  • URL parsing behavior can differ from modern browser expectations.

Use this shape:

URI uri = URI.create(rawInput);

Then validate components.

Avoid this for security policy:

URL url = new URL(rawInput);      // not the right policy primitive
boolean allowed = allowedUrls.contains(url);

4.1 Reject ambiguous forms

A safe egress parser should reject:

http://user:pass@example.com/
//example.com/path
http:example.com
file:///etc/passwd
gopher://example.com/
http://example.com#@internal
http://example.com:999999/
http://[::1]/

Do not “fix up” attacker input. Reject it.


5. Safe Egress Pipeline

A production-safe outbound request should pass through a pipeline.

The important ordering:

Do not attach credentials before the destination is authorized.


6. Allowlist Design

A blocklist is fragile because attackers have many ways to encode the same destination.

Prefer allowlists.

6.1 Bad allowlist

boolean allowed = rawUrl.startsWith("https://api.partner.com");

Bypass candidates include parser ambiguity, subdomain tricks, userinfo, redirects, and path confusion.

6.2 Better allowlist

Model a destination as structured policy:

record EgressDestinationPolicy(
    String id,
    Set<String> schemes,
    Set<String> exactHosts,
    Set<Integer> ports,
    List<String> pathPrefixes,
    boolean allowRedirects,
    long maxResponseBytes
) {}

Then compare parsed components, not strings.

6.3 Best allowlist: capability-based

Do not ask callers for a full URL when the destination is known.

Bad API:

fetch(String url)

Better API:

fetchPartnerInvoice(PartnerId partnerId, InvoiceId invoiceId)

The client builds the URL internally from trusted configuration:

URI uri = URI.create("https://api.partner.example/v1/invoices/" + encode(invoiceId));

The caller controls business identifiers, not network coordinates.

6.4 Allowlist patterns

PatternUse whenNotes
Exact host allowlistFixed partner APIsStrong default
Suffix allowlistControlled subdomainsRequire dot-boundary: .example.com, not string suffix only
Service registry IDInternal service callsPrefer service identity over user URL
Egress proxy routeEnterprise-controlled egressCentralizes enforcement
Signed callback URLTemporary controlled callbackInclude expiry, audience, and path constraints

7. Host Validation

7.1 Normalize host

Host comparison should account for:

  • case-insensitivity;
  • trailing dot in DNS names;
  • IDN/punycode;
  • IPv6 bracket syntax;
  • IPv4 literals;
  • percent encoding not allowed in host after parsing.

Example helper:

static String normalizeDnsHost(String host) {
    if (host == null || host.isBlank()) {
        throw new IllegalArgumentException("missing host");
    }

    String withoutTrailingDot = host.endsWith(".")
        ? host.substring(0, host.length() - 1)
        : host;

    String ascii = IDN.toASCII(withoutTrailingDot, IDN.USE_STD3_ASCII_RULES);
    return ascii.toLowerCase(Locale.ROOT);
}

7.2 Exact host match

static boolean exactHostAllowed(String host, Set<String> allowedHosts) {
    String normalized = normalizeDnsHost(host);
    return allowedHosts.contains(normalized);
}

7.3 Safe suffix match

Bad:

host.endsWith("example.com")

This permits evil-example.com.

Better:

static boolean isSubdomainOf(String host, String domain) {
    String h = normalizeDnsHost(host);
    String d = normalizeDnsHost(domain);
    return h.equals(d) || h.endsWith("." + d);
}

Use suffix allowlists only for domains you control.


8. Port and Scheme Policy

8.1 Scheme allowlist

For typical HTTP clients:

allowed schemes: https
maybe allowed: http for explicitly internal non-sensitive local development
blocked: file, jar, ftp, gopher, dict, ldap, mailto, data, javascript

Do not pass arbitrary schemes to protocol handlers.

8.2 Port allowlist

static int effectivePort(URI uri) {
    if (uri.getPort() != -1) return uri.getPort();
    return switch (uri.getScheme().toLowerCase(Locale.ROOT)) {
        case "https" -> 443;
        case "http" -> 80;
        default -> throw new IllegalArgumentException("unsupported scheme");
    };
}

Common safe policy:

https: 443 only
http: 80 only, if allowed at all

Avoid allowing arbitrary ports unless the business capability requires them.


9. Address Classification

Host allowlists are not enough. DNS names resolve to IP addresses, and IP addresses have semantics.

You must reject unsafe address ranges for untrusted outbound fetch.

9.1 Unsafe destination categories

CategoryExamples
Loopback127.0.0.0/8, ::1/128
Private IPv410.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
Link-local IPv4169.254.0.0/16
Link-local IPv6fe80::/10
Unique-local IPv6fc00::/7
Unspecified0.0.0.0, ::
Multicast224.0.0.0/4, ff00::/8
IPv4-mapped IPv6::ffff:127.0.0.1 style
Cloud metadatacommonly 169.254.169.254 and provider-specific names

9.2 Java InetAddress helpers

InetAddress provides useful methods:

static boolean isObviouslyUnsafe(InetAddress address) {
    return address.isAnyLocalAddress()
        || address.isLoopbackAddress()
        || address.isLinkLocalAddress()
        || address.isSiteLocalAddress()
        || address.isMulticastAddress();
}

But do not rely only on this for full SSRF defense. Maintain explicit CIDR ranges for your policy, especially for provider-specific metadata endpoints and IPv6 ranges.

9.3 Validate all resolved addresses

static List<InetAddress> resolveAndValidateAll(String host, Predicate<InetAddress> allowed) throws UnknownHostException {
    InetAddress[] addresses = InetAddress.getAllByName(host);
    if (addresses.length == 0) {
        throw new UnknownHostException("no addresses for " + host);
    }

    List<InetAddress> result = new ArrayList<>();
    for (InetAddress address : addresses) {
        if (!allowed.test(address)) {
            throw new SecurityException("unsafe resolved address: " + address.getHostAddress());
        }
        result.add(address);
    }
    return List.copyOf(result);
}

Why all addresses?

If a hostname resolves to both safe and unsafe addresses, the connection path may choose an unsafe one depending on address preference, retries, IPv4/IPv6 behavior, or runtime/network configuration.


10. DNS Rebinding and TOCTOU

DNS rebinding exploits the gap between:

validation time: host resolves to safe public IP
connection time: host resolves to internal/private IP

This is a time-of-check/time-of-use problem.

10.1 Weak approach

InetAddress address = InetAddress.getByName(host);
if (isPublic(address)) {
    httpClient.send(request, handler); // may resolve again internally later
}

Your validation resolution may not be the same resolution used by the HTTP client connection.

10.2 Stronger approaches

ApproachStrengthNotes
Exact host allowlist for trusted domainsHighBest for partner APIs
Egress proxy/firewall enforcementVery highBlocks unsafe IPs regardless of app bugs
Disable arbitrary URL fetchHighestUse capability-specific APIs
Custom DNS resolver / connect layerHigh but complexHard with high-level HttpClient
Revalidate redirects and resolved addressesNecessary but not sufficient aloneReduces common SSRF paths

The practical enterprise answer is layered:

application allowlist + safe parser + redirect validation + egress proxy/firewall + metadata endpoint protection

11. Redirect Safety

Redirects are destination changes.

If you validate only the first URL but automatically follow redirects, the actual request may go elsewhere.

11.1 Dangerous

HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.ALWAYS)
    .build();

Dangerous for untrusted destinations because Location may point to internal networks or unsafe schemes.

11.2 Safer

Use no automatic redirects, then manually validate each hop.

HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.NEVER)
    .connectTimeout(Duration.ofSeconds(3))
    .build();

Manual loop:

static HttpResponse<InputStream> sendWithValidatedRedirects(
    HttpClient client,
    URI initial,
    SafeEgressPolicy policy,
    int maxRedirects
) throws IOException, InterruptedException {
    URI current = initial;

    for (int i = 0; i <= maxRedirects; i++) {
        URI safe = policy.validate(current);

        HttpRequest request = HttpRequest.newBuilder(safe)
            .timeout(Duration.ofSeconds(10))
            .GET()
            .build();

        HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
        int status = response.statusCode();

        if (status >= 300 && status < 400) {
            response.body().close();
            Optional<String> location = response.headers().firstValue("Location");
            if (location.isEmpty()) {
                throw new IOException("redirect without Location");
            }
            current = safe.resolve(location.get());
            continue;
        }

        return response;
    }

    throw new IOException("too many redirects");
}

Each redirect target goes back through the full policy.


12. Credentials and Destination Approval

Never attach sensitive headers before destination validation.

Bad:

HttpRequest request = HttpRequest.newBuilder(userUri)
    .header("Authorization", "Bearer " + token)
    .GET()
    .build();

Better:

URI safeUri = policy.validate(userUri);

HttpRequest request = HttpRequest.newBuilder(safeUri)
    .header("Authorization", "Bearer " + token)
    .GET()
    .build();

Better still: avoid user URI entirely and map a business operation to a configured destination.

12.1 Credential scope rule

CredentialDestination rule
Partner API tokenexact partner API host only
Internal service mTLS identityinternal service identity only
User OAuth tokenresource server audience only
Basic authnever sent across redirect unless same approved origin
Cookiecontrolled cookie jar per approved origin

13. Proxy, Firewall, and Service Mesh Boundary

Application validation is necessary but insufficient.

A robust network boundary uses multiple enforcement layers:

13.1 Why app-only control fails

  • A future code path may bypass your wrapper.
  • A library may make its own HTTP call.
  • DNS rebinding may exploit client internals.
  • Redirect behavior may change.
  • Proxy configuration may differ across environments.
  • Emergency patches may introduce raw URL fetches.

13.2 Egress proxy benefits

  • central allowlist;
  • mTLS enforcement;
  • DNS policy;
  • audit logging;
  • block private/link-local ranges;
  • outbound rate limits;
  • consistent behavior across languages.

13.3 App still matters

The app should still validate because:

  • it knows business intent;
  • it can reject earlier and more clearly;
  • it can avoid credential leakage;
  • it can classify error reasons;
  • it can enforce payload limits and redirect policy.

14. Safe Egress Policy Skeleton

Below is a simplified skeleton. Real production code should use tested CIDR utilities and extensive test cases.

public final class SafeEgressPolicy {
    private final Set<String> allowedSchemes = Set.of("https");
    private final Set<String> allowedHosts;
    private final Set<Integer> allowedPorts = Set.of(443);

    public SafeEgressPolicy(Set<String> allowedHosts) {
        this.allowedHosts = allowedHosts.stream()
            .map(SafeEgressPolicy::normalizeDnsHost)
            .collect(Collectors.toUnmodifiableSet());
    }

    public URI validate(URI input) throws UnknownHostException {
        if (!input.isAbsolute() || input.isOpaque()) {
            throw new SecurityException("URI must be absolute and hierarchical");
        }

        String scheme = requireLower(input.getScheme(), "scheme");
        if (!allowedSchemes.contains(scheme)) {
            throw new SecurityException("scheme not allowed: " + scheme);
        }

        if (input.getRawUserInfo() != null) {
            throw new SecurityException("userinfo is not allowed");
        }

        if (input.getRawFragment() != null) {
            throw new SecurityException("fragment is not allowed");
        }

        String host = normalizeDnsHost(input.getHost());
        if (!allowedHosts.contains(host)) {
            throw new SecurityException("host not allowed: " + host);
        }

        int port = effectivePort(input);
        if (!allowedPorts.contains(port)) {
            throw new SecurityException("port not allowed: " + port);
        }

        validateResolvedAddresses(host);

        return canonicalize(input, scheme, host, port);
    }

    private static String requireLower(String value, String field) {
        if (value == null || value.isBlank()) {
            throw new SecurityException("missing " + field);
        }
        return value.toLowerCase(Locale.ROOT);
    }

    private static String normalizeDnsHost(String host) {
        if (host == null || host.isBlank()) {
            throw new SecurityException("missing host");
        }
        String h = host.endsWith(".") ? host.substring(0, host.length() - 1) : host;
        return IDN.toASCII(h, IDN.USE_STD3_ASCII_RULES).toLowerCase(Locale.ROOT);
    }

    private static int effectivePort(URI uri) {
        if (uri.getPort() != -1) return uri.getPort();
        return switch (uri.getScheme().toLowerCase(Locale.ROOT)) {
            case "https" -> 443;
            case "http" -> 80;
            default -> throw new SecurityException("unsupported scheme");
        };
    }

    private static void validateResolvedAddresses(String host) throws UnknownHostException {
        InetAddress[] addresses = InetAddress.getAllByName(host);
        if (addresses.length == 0) {
            throw new UnknownHostException(host);
        }
        for (InetAddress address : addresses) {
            if (isUnsafe(address)) {
                throw new SecurityException("unsafe resolved address: " + address.getHostAddress());
            }
        }
    }

    private static boolean isUnsafe(InetAddress address) {
        return address.isAnyLocalAddress()
            || address.isLoopbackAddress()
            || address.isLinkLocalAddress()
            || address.isSiteLocalAddress()
            || address.isMulticastAddress()
            || isCloudMetadataAddress(address)
            || isUniqueLocalIpv6(address);
    }

    private static boolean isCloudMetadataAddress(InetAddress address) {
        return address.getHostAddress().equals("169.254.169.254");
    }

    private static boolean isUniqueLocalIpv6(InetAddress address) {
        byte[] bytes = address.getAddress();
        return bytes.length == 16 && (bytes[0] & (byte) 0xfe) == (byte) 0xfc;
    }

    private static URI canonicalize(URI input, String scheme, String host, int port) {
        try {
            int explicitPort = switch (scheme) {
                case "https" -> port == 443 ? -1 : port;
                case "http" -> port == 80 ? -1 : port;
                default -> port;
            };
            return new URI(
                scheme,
                null,
                host,
                explicitPort,
                input.getRawPath() == null || input.getRawPath().isBlank() ? "/" : input.getRawPath(),
                input.getRawQuery(),
                null
            );
        } catch (URISyntaxException e) {
            throw new SecurityException("failed to canonicalize URI", e);
        }
    }
}

This skeleton demonstrates structure, not final completeness. Production code should include:

  • CIDR range library;
  • IPv4-mapped IPv6 handling;
  • provider-specific metadata hostnames;
  • redirect tests;
  • IDN tests;
  • parser-confusion tests;
  • proxy integration;
  • complete audit logging;
  • centralized configuration.

15. Special Cases

15.1 Cloud metadata endpoints

Many cloud environments expose metadata through link-local addresses or special hostnames.

Treat metadata access as its own privileged capability.

Rules:

  • block link-local destinations for arbitrary fetch;
  • block well-known metadata IPs and hostnames;
  • configure cloud metadata protections where available;
  • do not let generic HTTP clients reach metadata endpoints;
  • alert on denied attempts.

15.2 Kubernetes and service DNS

Inside Kubernetes, names like these may resolve internally:

service
service.namespace
service.namespace.svc
service.namespace.svc.cluster.local

A URL fetcher that seems safe in local development may become dangerous inside a cluster.

Policy should be environment-aware and enforced at network level.

15.3 Localhost is not safe

localhost can expose:

  • admin endpoints;
  • metrics with secrets;
  • local proxies;
  • database tunnels;
  • sidecar APIs;
  • debug ports.

Reject loopback unless the specific business capability requires it and is isolated.

15.4 Private IPs are not always obviously private

Attackers may use:

  • decimal IPv4 forms;
  • octal-like notations in some parsers;
  • hexadecimal notation in some contexts;
  • IPv4-mapped IPv6;
  • DNS names resolving to private IPs;
  • redirects to private IPs;
  • CNAME chains.

Use strict parser behavior and address-based validation after DNS resolution.


16. Response Handling Is Also Security

Safe egress is not only about destination. Response handling matters too.

Apply limits from Part 025:

  • max response bytes;
  • max header size where server/framework permits;
  • max redirects;
  • max decompressed size;
  • content-type allowlist;
  • streaming instead of full buffering;
  • no raw internal error response passthrough;
  • malware/scanner boundary if importing files.

16.1 Decompression bomb risk

If you accept compressed responses, the compressed byte size may be small while decompressed content is huge.

Policy should define:

maxCompressedBytes
maxDecompressedBytes
allowedContentEncodings

16.2 Content type is not trust

Do not trust Content-Type alone. It is a hint. Validate actual content if it crosses security boundaries.


17. Observability and Audit

Security controls must be debuggable without leaking secrets.

17.1 Decision log fields

Log:

policyId
operation
normalizedScheme
normalizedHost
effectivePort
pathPolicyMatched
resolvedAddressClasses
decision
reason
callerService
traceId

Avoid logging:

  • full query strings with secrets;
  • authorization headers;
  • cookies;
  • raw user-submitted URLs when they may contain credentials;
  • response bodies.

17.2 Metrics

MetricMeaning
egress.policy.allowedapproved outbound attempts
egress.policy.denieddenied outbound attempts
egress.policy.denied.reasoncardinality-controlled reason tag
egress.redirect.deniedredirect blocked by policy
egress.resolution.unsafeDNS resolved to unsafe address
egress.response.too_largeresponse exceeded byte cap
egress.destination.hostuse low-cardinality grouping, not raw arbitrary hosts

18. Test Matrix

A safe egress wrapper needs adversarial tests.

18.1 URI parsing tests

InputExpected
https://api.partner.example/resourceallow if host is configured
http://api.partner.example/resourcedeny if only HTTPS allowed
file:///etc/passwddeny
https://user:pass@api.partner.example/deny
https://api.partner.example:444/deny unless port allowed
//api.partner.example/pathdeny relative network-path reference
https://api.partner.example#fragmentdeny or strip only by explicit policy

18.2 Host confusion tests

InputExpected
https://evil-api.partner.example.attacker.com/deny
https://api.partner.example./normalize and evaluate
https://API.PARTNER.EXAMPLE/normalize and evaluate
https://xn--.../IDN policy applied
https://127.0.0.1/deny
https://[::1]/deny
https://169.254.169.254/deny

18.3 DNS tests

ScenarioExpected
allowed host resolves to public IPallow
allowed host resolves to private IPdeny
allowed host resolves to mixed public/private IPsdeny
DNS changes between callsrevalidation catches unsafe result where possible
CNAME chain ends in unsafe IPdeny based on final addresses

18.4 Redirect tests

ScenarioExpected
redirect to same approved hostallow if redirects enabled
redirect from approved host to private IPdeny
redirect from HTTPS to HTTPdeny unless policy allows downgrade
redirect with userinfodeny
redirect loopfail at max redirect count

19. Architecture Decision Record Template

Use this ADR shape for safe egress decisions.

# ADR: Safe Egress Policy for <Capability>

## Context

The service needs to call <destination> for <business purpose>.
The destination may be influenced by <trusted config / user input / partner data>.

## Decision

- Allowed schemes:
- Allowed hosts:
- Allowed ports:
- Redirect policy:
- DNS/address policy:
- Credential attachment point:
- Response size limit:
- Proxy/egress gateway:
- Audit fields:

## Rejected Alternatives

- Raw user-provided URL:
- Blocklist-only validation:
- Automatic redirects:

## Failure Behavior

- Unsafe destination:
- DNS failure:
- Redirect denied:
- Response too large:
- Timeout:

## Tests

- Parser confusion:
- Private IP:
- Metadata endpoint:
- Redirect abuse:
- Large response:

This forces security choices to become explicit and reviewable.


20. Anti-Patterns

20.1 “We validate it with regex”

Regex is not a URI parser. Use structured parsing, then policy.

20.2 “We block localhost”

Blocking only localhost misses 127.0.0.1, ::1, private ranges, link-local addresses, DNS rebinding, redirects, and metadata endpoints.

20.3 “The firewall protects us”

Maybe. But the application still may leak credentials, follow unsafe redirects, or generate confusing failures. Use both app and network controls.

20.4 “It is only an image fetcher”

Image fetchers are classic SSRF surfaces. They fetch attacker-chosen URLs and often process large or malformed content.

20.5 “We trust DNS”

DNS is a name-to-address mechanism, not an authorization system.

20.6 “We use HTTPS, so it is safe”

HTTPS protects transport authenticity/confidentiality for the chosen destination. It does not prove the destination is allowed.


21. Production Checklist

21.1 API design

  • Does the API avoid accepting arbitrary URLs where possible?
  • Is the destination derived from trusted configuration?
  • Is the business capability mapped to a named egress policy?
  • Are credentials attached only after approval?

21.2 URI validation

  • Is URI used for parsing?
  • Are relative, opaque, malformed, and userinfo URIs rejected?
  • Are scheme and port allowlisted?
  • Is host normalized?
  • Is suffix matching dot-boundary safe?

21.3 DNS/IP validation

  • Are all resolved addresses checked?
  • Are loopback, private, link-local, multicast, unspecified, and metadata ranges blocked?
  • Are IPv6 and IPv4-mapped forms covered?
  • Is DNS rebinding considered?

21.4 Redirects

  • Are automatic redirects disabled for untrusted destinations?
  • Is every redirect target revalidated?
  • Is redirect count bounded?
  • Are scheme downgrades denied?

21.5 Network enforcement

  • Is there an egress proxy, firewall, service mesh, or network policy?
  • Are private and metadata ranges blocked outside the app too?
  • Are emergency bypasses audited?

21.6 Response safety

  • Is max response size enforced?
  • Is decompressed size bounded?
  • Is content streamed safely?
  • Are raw responses not reflected to users?

21.7 Observability

  • Are allow/deny decisions logged safely?
  • Are deny reasons metricized?
  • Are suspicious attempts alertable?
  • Are raw secrets redacted?

22. Deliberate Practice

Drill 1 — Safe URL validator

Implement SafeEgressPolicy.validate(URI) with tests for:

  • scheme;
  • host;
  • port;
  • userinfo;
  • fragment;
  • IDN;
  • private IP;
  • loopback;
  • metadata endpoint;
  • redirect target.

Drill 2 — Manual redirect client

Build an HttpClient wrapper that:

  • disables automatic redirect;
  • validates each Location;
  • caps redirect count;
  • preserves method policy correctly;
  • refuses credential forwarding across origin.

Drill 3 — DNS rebinding lab

Create a test resolver or local DNS setup that returns:

  1. public IP on first lookup;
  2. private IP on second lookup.

Verify what your app catches and what only network-level enforcement can catch.

Drill 4 — Egress audit dashboard

Expose:

  • top denied policy reasons;
  • denied unsafe address classes;
  • redirect denials;
  • metadata endpoint attempts;
  • policy id by operation.

Your goal is to make unsafe egress attempts visible before they become incidents.


23. Mental Model Summary

Safe egress is the discipline of preventing a Java service from becoming an attacker-controlled network pivot.

Remember:

  1. Outbound network access is a capability.
  2. User input should not directly become a destination.
  3. Parse, normalize, authorize, resolve, validate addresses, then connect.
  4. Validate every redirect as a new destination.
  5. Block unsafe IP ranges even when the hostname looks acceptable.
  6. DNS is not authorization.
  7. App-level validation and network-level enforcement are complementary.
  8. Credentials are attached only after destination approval.
  9. Large responses and decompression are also part of egress risk.
  10. A safe client is a policy boundary, not just a wrapper around HttpClient.

The senior-engineering question is:

What exact network destinations can this code cause our infrastructure to reach, under whose authority, and what prevents that set from expanding accidentally?


References

Lesson Recap

You just completed lesson 26 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.