Introduction
Every time your machine resolves a hostname - whether you're opening a browser, running a curl command, or starting a Docker container - the operating system consults a resolution chain before it ever reaches a DNS server. At the very top of that chain, on virtually every Unix-like system and on Windows, sits a small plain-text file: /etc/hosts.
It has no daemon, no cache, no TTL, and no replication mechanism. It is read directly from disk, line by line, and matched against the hostname being looked up. If a match is found, resolution stops there - no DNS query is emitted, no network traffic occurs, no round-trip latency is incurred. The result is returned immediately and treated as authoritative. This simplicity is both its greatest strength and, in the wrong hands, its greatest liability.
For most developers, /etc/hosts is something they edit once to set up a local development domain, then forget about. But understanding how it actually fits into the name service switch (NSS) stack, how it interacts with containers, CDN bypass testing, TLS verification, and microservice routing reveals a surprisingly deep surface area. This article is a complete technical examination of that surface area.
Historical Context: Before DNS Was Invented
To understand why /etc/hosts exists and why it still persists in its current form, you have to go back to the early ARPANET. In the 1970s, the entire network was small enough that a single authoritative text file - HOSTS.TXT - maintained by the Stanford Research Institute Network Information Center (SRI-NIC) could map every known hostname to its IP address. Administrators would periodically FTP this file from SRI-NIC and replace their local copy.
By the early 1980s, the network had grown to thousands of hosts, and the SRI-NIC model was visibly collapsing under the load. Update latency was measured in days. Name collisions between different organizations had become routine. RFC 882 (1983) and subsequently RFC 1034/1035 (1987) introduced the Domain Name System as a hierarchical, distributed, delegated replacement. DNS solved the scalability problem. But /etc/hosts was already present on every machine and deeply embedded in system administration workflows, so rather than being removed, it was retained as a local override layer.
The modern /etc/hosts file is a direct descendant of that original HOSTS.TXT. Its format - IP address, whitespace, one or more hostnames per line, # for comments - has been essentially unchanged for four decades. What has changed is the layer it sits within: today it is one input to a broader name service switch framework, but it remains the first input consulted by default on nearly every system.
How /etc/hosts Fits Into the Name Service Switch
Understanding /etc/hosts in isolation misses its context. On modern Linux systems, name resolution is governed by the Name Service Switch (NSS), configured in /etc/nsswitch.conf. The hosts line in that file defines the ordered sequence of sources the system consults when resolving a hostname.
A typical default looks like this:
hosts: files dns
The files source is /etc/hosts. The dns source is the resolver configured in /etc/resolv.conf. The order matters: with the default configuration, /etc/hosts is consulted first, and a match there is terminal - the resolver will not emit a DNS query for that name under any circumstance. If you want DNS to take precedence, you can reorder to dns files, though this is rarely done in practice and may have unintended consequences for localhost and loopback resolution.
macOS uses a conceptually similar but implementation-distinct system. The DirectoryService (and its successor, opendirectoryd) handles resolution via a framework called the Common Security Services Manager (CSSM), but /etc/hosts is still checked first via the same NSS-compatible mechanism. On Windows, the equivalent file lives at C:\Windows\System32\drivers\etc\hosts and is consulted before the DNS client service, following the same priority semantics.
One important subtlety: tools that bypass the standard C library resolver (getaddrinfo, gethostbyname) will not consult /etc/hosts. Programs that implement their own DNS stack - certain Go binaries, applications using c-ares, or custom DNS-over-HTTPS clients - may skip the NSS layer entirely. This is a common source of confusion when a developer adds an entry to /etc/hosts and finds that a particular application doesn't honor it.
File Format, Syntax, and Parsing Rules
The format of /etc/hosts is deliberately minimal. Each non-comment line contains an IP address followed by one or more hostnames separated by whitespace. The first hostname on the line is treated as the canonical name; subsequent entries are aliases. Comment lines begin with #.
# Loopback addresses
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
# Development overrides
127.0.0.1 api.myapp.local myapp.local
192.168.1.50 staging.internal
# Block known ad domains by pointing to null route
0.0.0.0 ads.example.com
A few non-obvious parsing behaviors are worth understanding in detail. Trailing whitespace on a line is ignored. The file is case-insensitive for hostname matching on most implementations, consistent with DNS case-insensitivity as specified in RFC 4343. IPv6 addresses are fully supported and are disambiguated from hostnames by the : character. There is no limit on the number of aliases per line in most implementations, though extremely long lines have historically caused issues on some embedded or legacy systems.
One subtlety that causes real bugs: wildcard matching is not supported. An entry like 127.0.0.1 *.myapp.local will not match api.myapp.local. The asterisk is treated as a literal hostname character. This is a hard constraint with no workaround at the /etc/hosts level - for wildcard local domains, you need a local DNS resolver like dnsmasq or systemd-resolved with appropriate wildcard rules. Developers who expect wildcard behavior and don't get it often waste significant time debugging what appears to be a DNS propagation issue.
Practical Engineering Patterns
Local Development Environments
The most common use of /etc/hosts in a developer workflow is mapping friendly hostnames to 127.0.0.1 or a specific local network address. This enables local HTTPS development with a domain that matches production, which is increasingly necessary as browsers restrict features to secure contexts.
# /etc/hosts additions for a typical local dev setup
127.0.0.1 app.mycompany.local
127.0.0.1 api.mycompany.local
127.0.0.1 auth.mycompany.local
Combined with a locally trusted TLS certificate (via mkcert, for example), this approach allows developers to test cookie behavior, CORS policies, and service worker registration against realistic domain structures rather than raw localhost. The /etc/hosts entry provides the DNS resolution; a reverse proxy (nginx, Caddy, or Traefik running locally) provides the TLS termination and routing.
Testing CDN and DNS Propagation
When you need to verify that a new server configuration is correct before DNS propagates, or before a CDN cache flushes, pointing a specific hostname to the target IP via /etc/hosts lets you test the live server as if propagation had completed. This is a standard technique in deployment workflows.
# Point production hostname to new server for local testing
# before actual DNS cutover
203.0.113.42 www.mycompany.com
With this in place, all local tooling - curl, your browser, your test suite - will route to the new server. You can verify TLS certificate validity, application behavior, and health checks in isolation from the live DNS record. This is considerably safer than making DNS changes speculatively, and it leaves no footprint in the infrastructure.
Service Mocking in Integration Tests
In integration test suites, you sometimes need to resolve a production-like hostname to a mock or stub server running locally. Rather than parameterizing your entire application to support configurable hostnames, a test harness can temporarily modify /etc/hosts (or use the OS-level override in containerized test environments) to route specific names to test doubles.
import subprocess
import tempfile
import os
from contextlib import contextmanager
@contextmanager
def mock_host(hostname: str, ip: str = "127.0.0.1"):
"""
Temporarily add a /etc/hosts entry for the duration of a test.
Requires elevated privileges or a writable hosts file in CI.
Restores the original file on exit, even if the test fails.
"""
hosts_path = "/etc/hosts"
entry = f"{ip}\t{hostname}\n"
with open(hosts_path, "r") as f:
original_content = f.read()
try:
with open(hosts_path, "a") as f:
f.write(entry)
yield
finally:
with open(hosts_path, "w") as f:
f.write(original_content)
This pattern works well in Docker-based CI environments where the container runs as root. In production-like environments where you don't have root access, the alternative is to configure your application's HTTP client to use a custom resolver - which is a cleaner long-term architecture but requires more application-level investment.
Blocking Unwanted Traffic
A well-known technique for blocking ad servers, tracking domains, or malware callback endpoints is to point their hostnames to 0.0.0.0 or 127.0.0.1 in /etc/hosts. Projects like the Steven Black hosts file aggregate hundreds of thousands of known malicious and advertising domains into a single drop-in /etc/hosts block.
0.0.0.0 doubleclick.net
0.0.0.0 googleadservices.com
0.0.0.0 analytics.google.com
Using 0.0.0.0 rather than 127.0.0.1 is slightly preferable: connections to 127.0.0.1 will be immediately refused if no server is listening there, producing a fast failure, but 0.0.0.0 causes the TCP stack to refuse the connection immediately without the OS needing to route it at all on most implementations. In practice the difference is negligible for most use cases.
Containers, Kubernetes, and the Modern Complication
Docker and Kubernetes have introduced new dimensions to /etc/hosts management that catch engineers off guard. By default, Docker injects a minimal /etc/hosts into each container at startup, mapping the container's own hostname and the gateway IP. This file is ephemeral - it exists only for the life of the container and is distinct from the host machine's /etc/hosts.
# Inside a running Docker container
cat /etc/hosts
# 127.0.0.1 localhost
# ::1 localhost ip6-localhost ip6-loopback
# 172.17.0.2 a1b2c3d4e5f6 my-container-name
In Docker Compose, you can inject additional entries using the extra_hosts directive:
services:
api:
image: my-api:latest
extra_hosts:
- "legacy-db.internal:192.168.1.100"
- "auth-service.internal:192.168.1.101"
In Kubernetes, the equivalent is the hostAliases field on a Pod spec:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
hostAliases:
- ip: "192.168.1.100"
hostnames:
- "legacy-db.internal"
containers:
- name: app
image: my-app:latest
A critical point for Kubernetes operators: hostAliases injects entries into the Pod's /etc/hosts file at startup, but those entries are not reflected in the cluster's CoreDNS. Other Pods in the cluster cannot resolve legacy-db.internal just because you configured it on one Pod. If you need cluster-wide name resolution, you need to configure CoreDNS with a custom stub zone or use a Service with an explicit DNS name, not a hostAliases entry.
Security Implications and Attack Surface
The /etc/hosts file is a privileged resource, but it is not always treated as one. Its permissions on a correctly configured Linux system are 0644 (world-readable, root-writable), but misconfigurations, container escape vectors, and supply chain compromises have all been used to write malicious entries to it.
The most straightforward attack vector is DNS hijacking via /etc/hosts modification. If an attacker gains write access to this file, they can redirect any hostname - including api.github.com, registry.npmjs.org, or your company's internal identity provider - to a server they control. From that server, they can serve fake content, capture credentials, or return malicious package updates. Unlike DNS-level attacks, this vector bypasses DNSSEC entirely, since /etc/hosts is never DNSSEC-validated.
This has practical consequences for supply chain security. A malicious npm install script or a container with an overly permissive volume mount could write a registry.npmjs.org redirect into /etc/hosts, causing all subsequent package installs in the same environment to pull from a rogue registry. The defense is straightforward in principle - audit file permissions, use read-only bind mounts in container environments, and monitor the file for unexpected changes - but it requires active attention.
Another less-obvious implication involves TLS certificate validation. A /etc/hosts override can route traffic to an attacker's server, but if that server presents a TLS certificate that does not match the hostname, the connection will fail (assuming the client validates certificates, as it should). This means a /etc/hosts attack is most dangerous either when the attacker also has a valid certificate for the redirected hostname (possible with Let's Encrypt if they control a domain that matches), or when the targeted application disables certificate validation - a common anti-pattern in development environments that occasionally leaks into production.
Trade-offs and Common Pitfalls
No Wildcards
As noted in the format section, wildcards are not supported. This is probably the most common source of frustration. If you have a microservices architecture where each service exposes a subdomain (user-service.local, order-service.local, etc.), you need a separate /etc/hosts line for each one. dnsmasq with a simple config (address=/.local/127.0.0.1) is the right tool for this use case, not manual /etc/hosts management.
No TTL and Cache Invalidation
DNS responses have TTLs that determine how long resolvers cache them. /etc/hosts has no TTL concept. Changes take effect immediately for new connections, but some applications cache their own resolution results in process memory. Long-running JVM applications, for instance, have historically cached DNS lookups indefinitely by default (controlled by sun.net.inetaddr.ttl). Adding or modifying a /etc/hosts entry may not take effect for such an application until it is restarted.
The "Works on My Machine" Trap
If your development workflow depends on /etc/hosts entries that are not documented or scripted, you have introduced invisible, machine-specific configuration that is not captured in version control. New team members will hit resolution failures that are difficult to diagnose without knowing to look in /etc/hosts. The discipline is to either document these entries explicitly in a CONTRIBUTING.md or README.md, or better yet, provide a script that idempotently ensures the required entries are present.
#!/usr/bin/env bash
# setup-hosts.sh - Idempotently ensures development /etc/hosts entries are present
ENTRIES=(
"127.0.0.1 api.myapp.local"
"127.0.0.1 app.myapp.local"
"127.0.0.1 auth.myapp.local"
)
for entry in "${ENTRIES[@]}"; do
if ! grep -qF "$entry" /etc/hosts; then
echo "Adding: $entry"
echo "$entry" | sudo tee -a /etc/hosts > /dev/null
else
echo "Already present: $entry"
fi
done
Conflicting Entries
The file is parsed top to bottom and the first match wins. If you have two entries for the same hostname pointing to different IPs, only the first one is honored. This is not an error condition - the system will not warn you. It is entirely possible to spend an hour debugging why your CDN bypass test isn't working before noticing a duplicate entry from an old development setup at the top of the file.
IPv6 and Dual-Stack Ambiguity
Modern systems support both IPv4 and IPv6 resolution. If your /etc/hosts only has an IPv4 entry for a hostname but the application or resolver prefers IPv6 (e.g., via getaddrinfo with AI_ADDRCONFIG or on a system where IPv6 is preferred by default), the lookup may fall through to DNS for the AAAA record and resolve to a different address than you intended. Always include both an A (IPv4) and AAAA (IPv6) entry when you want guaranteed local resolution regardless of the address family requested.
Best Practices
Keep /etc/hosts modifications minimal and purposeful. The file is a shared system resource; accumulating years of stale development overrides makes it an operational liability. Periodically audit the non-standard entries and remove anything that is no longer needed. On shared development machines or CI systems, treat it as infrastructure configuration - tracked, versioned, and applied via automation.
Use dnsmasq, systemd-resolved, or a local CoreDNS instance when your needs outgrow simple host overrides. Any use case involving wildcards, dynamic hostname registration, or multi-service development environments belongs in a proper local DNS resolver, not a growing list of /etc/hosts entries. The threshold is roughly five or more related entries pointing to the same IP - at that point, a wildcard rule in dnsmasq is both simpler and less error-prone.
In containerized and cloud-native environments, prefer Kubernetes Services and DNS-based service discovery over hostAliases. The hostAliases mechanism is an escape hatch for legacy compatibility, not a primary service discovery strategy. It creates implicit, hard-coded IP dependencies in your Pod specs that violate the principle of environment-independence.
Monitor the file for unexpected changes in security-sensitive environments. On Linux, inotifywait can watch the file:
inotifywait -m /etc/hosts -e modify,attrib,close_write 2>/dev/null |
while read path action file; do
echo "$(date): /etc/hosts was modified (event: $action)" | \
logger -t hosts-monitor -p security.warning
done
Integrating this into your system's security event pipeline gives you an audit trail and an alerting surface for one of the most overlooked lateral movement vectors in post-exploitation scenarios.
80/20 Insight
Most of the operational value from /etc/hosts comes from three patterns: local development domain aliases (enabling realistic TLS and cookie testing), CDN/DNS bypass testing before cutovers (reducing deployment risk), and blocking unwanted external communication (for security or productivity filtering). These three use cases cover the vast majority of legitimate /etc/hosts engineering. Everything else - complex service mesh overrides, Kubernetes hostAliases for production routing, wildcard attempts - should trigger a design review to determine whether a proper DNS solution is warranted instead.
The single most important thing to understand about /etc/hosts is that it is terminal and authoritative when consulted. There is no fallback, no cache expiry, no error propagation to DNS. If an entry is wrong, it will silently produce wrong results for every application using the standard resolver on that machine. This makes correctness and cleanliness of the file a higher-stakes concern than its plaintext simplicity might suggest.
11. Key Takeaways
- Audit your current
/etc/hostsbefore relying on it - duplicate, stale, or conflicting entries silently produce wrong resolution results with no error messages. - Script your development entries - document and automate any
/etc/hostsadditions your project requires, so onboarding is repeatable and the dependency is visible. - Use
dnsmasqorsystemd-resolvedfor wildcard or multi-service local domains -/etc/hostsdoes not support wildcards; fighting this constraint is always the wrong approach. - Include both IPv4 and IPv6 entries when you need guaranteed override behavior on dual-stack systems.
- Treat
/etc/hostsas a security surface - on any system where unauthorized writes are a concern, monitor it with filesystem event tooling and include it in your threat model.
Conclusion
/etc/hosts is a relic of the pre-DNS internet that has outlasted every prediction of its obsolescence. Its persistence is not accidental - it fills a genuine need for authoritative, zero-latency, zero-dependency local name resolution that no other mechanism in the stack provides. For development workflows, security testing, and deployment verification, it remains the most direct and reliable tool available.
The risk it carries is proportional to how casually it is treated. Because it is a plain text file that any text editor can modify, it tends to accumulate technical debt silently. Because it takes absolute priority over DNS, mistakes in it are always wrong, never approximately right. And because it sits below the application layer, no amount of application-level debugging will reveal that the problem is a stale entry in a file you edited eight months ago.
Treat it with the same discipline you'd apply to any other piece of infrastructure configuration: minimal, documented, version-controlled where possible, monitored in production. Understood properly, it is a precise and powerful tool. Treated carelessly, it is one of the most effective ways to lose an afternoon to a problem that has nothing to do with your code.
References
- RFC 1034 - Domain Names: Concepts and Facilities. P. Mockapetris, November 1987. https://datatracker.ietf.org/doc/html/rfc1034
- RFC 1035 - Domain Names: Implementation and Specification. P. Mockapetris, November 1987. https://datatracker.ietf.org/doc/html/rfc1035
- RFC 4343 - Domain Name System (DNS) Case Insensitivity Clarification. D. Eastlake, January 2006. https://datatracker.ietf.org/doc/html/rfc4343
- RFC 882 - Domain Names: Concepts and Facilities (original). P. Mockapetris, November 1983. https://datatracker.ietf.org/doc/html/rfc882
- The Linux man page: hosts(5) - https://man7.org/linux/man-pages/man5/hosts.5.html
- The Linux man page: nsswitch.conf(5) - https://man7.org/linux/man-pages/man5/nsswitch.conf.5.html
- Kubernetes Documentation: Pod hostAliases - https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/
- Docker Documentation: extra_hosts - https://docs.docker.com/compose/compose-file/05-services/#extra_hosts
- Steven Black's Hosts Project - Unified hosts file aggregating multiple reputable sources. https://github.com/StevenBlack/hosts
- mkcert - A simple tool for making locally-trusted development certificates. https://github.com/FiloSottile/mkcert
- dnsmasq documentation - http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html
- CoreDNS documentation - https://coredns.io/manual/toc/
- Salus, J. H. - A Quarter Century of UNIX. Addison-Wesley, 1994. (Historical context on ARPANET and early networking infrastructure.)