The Core Principle: Identity Over Network Location
Traditional perimeter security trusts traffic that arrives from inside the network. Zero trust discards that assumption entirely. A process running on 10.0.0.5 gets no more implicit trust than one connecting from the public internet. Every request must carry a verifiable identity - cryptographic, not IP-based.
On Linux, identity can be established at multiple layers: X.509 certificates for service-to-service TLS, SPIFFE/SPIRE for workload identity in containerized environments, Kerberos tickets for legacy enterprise systems, or JWT tokens validated at the application layer. The key is that identity must be unforgeable and short-lived. We use SPIRE with certificate TTLs of 1 hour on our test cluster.
The practical starting point is auditing your current implicit trust. Run `ss -tulnp` and `netstat -an` to enumerate every listening socket. Any service that accepts connections without authentication is a zero trust violation regardless of whether it is only reachable internally. In our experience, most fresh Linux server deployments have at least 6-8 unauthenticated internal listeners.
# Enumerate all listening services with their processes
ss -tulnp
# Check which services have no TLS
nmap --script ssl-cert -p 1-65535 localhost
# List all open connections (shows implicit trust surface)
ss -anp | grep ESTABLISHED | awk '{print $5}' | sort | uniq -c | sort -rn
SPIFFE and SPIRE: Workload Identity That Scales
SPIFFE (Secure Production Identity Framework for Everyone) defines a standard for workload identity using SVIDs (SPIFFE Verifiable Identity Documents). SPIRE is the production implementation. Each workload gets a cryptographic identity in the form of `spiffe://trust-domain/path` backed by a short-lived X.509 certificate or JWT token.
Install the SPIRE server and agent on RHEL 9.4 or Ubuntu 24.04 using the official binaries from the SPIRE 1.10.x release series. The server acts as the CA; agents run on each node and attest workloads using kernel selectors like PID, UID, or container image digest.
The SPIRE agent uses node attestation to verify that the machine itself is legitimate before issuing any workload identities. On AWS this uses the EC2 instance identity document; on bare metal you use join tokens or TPM attestation. After attestation, the agent queries the server via a local Unix socket at `/tmp/spire-agent/public/api.sock`.
Once SPIRE is running, applications fetch their SVID through the SPIFFE Workload API. The `spiffe-helper` sidecar can write the certificate and private key to disk so legacy applications that read PEM files get automatic rotation without code changes.
# Download SPIRE 1.10.1
curl -Lo spire-1.10.1-linux-amd64.tar.gz \
https://github.com/spiffe/spire/releases/download/v1.10.1/spire-1.10.1-linux-amd64.tar.gz
tar xzf spire-1.10.1-linux-amd64.tar.gz
# Minimal SPIRE server config
cat > /etc/spire/server.conf << 'EOF'
server {
bind_address = "0.0.0.0"
bind_port = "8081"
trust_domain = "myunix.example"
data_dir = "/var/lib/spire/server"
log_level = "INFO"
ca_ttl = "24h"
default_svid_ttl = "1h"
}
plugins {
DataStore "sql" {
plugin_data {
database_type = "sqlite3"
connection_string = "/var/lib/spire/server/datastore.sqlite3"
}
}
NodeAttestor "join_token" {
plugin_data {}
}
KeyManager "memory" {
plugin_data {}
}
}
EOF
# Start the server
/opt/spire/bin/spire-server run -config /etc/spire/server.conf &
# Create a join token for agent registration
/opt/spire/bin/spire-server token generate -spiffeID spiffe://myunix.example/agent/node1
Mutual TLS Between Services: The Minimum Viable Zero Trust Connection
mTLS means both client and server present certificates and verify each other. This eliminates the "trusted network" assumption because even if someone is on the same VLAN, they cannot connect without a valid certificate signed by your CA.
For internal service communication without a full service mesh, you can enforce mTLS using nginx or HAProxy as a sidecar. The following nginx config requires client certificate verification and only accepts TLS 1.3, which eliminates a large class of downgrade attacks.
For services you control the code of, use Go's `crypto/tls` or Python's `ssl` module with `CERT_REQUIRED` mode. Set `MinVersion: tls.VersionTLS13` and verify the peer certificate's SPIFFE URI SAN against your trust domain. Do not just verify that a certificate is signed by your CA - verify that the SPIFFE ID matches what that specific caller is expected to present.
In our test environment, adding mTLS between 8 Python microservices added 0.3ms average latency per connection on localhost. For connection-pooled long-lived connections the overhead is essentially zero after the handshake.
# nginx mTLS sidecar config
server {
listen 8443 ssl;
ssl_certificate /etc/spire/certs/server.crt;
ssl_certificate_key /etc/spire/certs/server.key;
ssl_client_certificate /etc/spire/certs/ca.crt;
ssl_verify_client on;
ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
ssl_verify_depth 2;
# Only allow known SPIFFE IDs via header inspection
# (set by SPIRE helper in X-SPIFFE-ID header)
location / {
if ($ssl_client_s_dn !~* "spiffe://myunix.example/service/") {
return 403;
}
proxy_pass http://127.0.0.1:8080;
}
}
# Test mTLS connection
curl --cert /etc/spire/certs/client.crt \
--key /etc/spire/certs/client.key \
--cacert /etc/spire/certs/ca.crt \
https://service-a.internal:8443/health
eBPF Network Policy: Kernel-Level Enforcement
eBPF programs attached to TC (traffic control) hooks enforce network policy at the kernel level, before packets reach user space. Cilium uses this to implement identity-based network policy without iptables. On a 4-node Kubernetes cluster running Cilium 1.16, we measured policy enforcement overhead of under 5 microseconds per packet.
For non-Kubernetes Linux hosts, you can write targeted eBPF programs using `bpftool` and `libbpf`, or use Falco's eBPF-based syscall monitoring alongside `tc` rules. The practical approach for most teams is Cilium for container workloads and nftables with strict rules for bare metal.
With nftables, implement a default-deny policy and explicitly allow only required flows. Unlike iptables, nftables handles both IPv4 and IPv6 in a single ruleset and has atomic rule replacement, which matters when you are updating policy under load.
The key zero trust principle in network policy is: deny everything, allow only what you have explicitly modeled. Start with `nft flush ruleset` on a test interface, build your allowlist, and measure what breaks. Do not add "temporary" allow-all rules - they become permanent.
# Minimal nftables zero trust ruleset
# /etc/nftables.conf
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow established connections
ct state established,related accept
# Allow loopback
iif lo accept
# Allow SSH from management subnet only
ip saddr 10.0.1.0/24 tcp dport 22 accept
# Allow SPIRE agent to server communication
ip daddr 10.0.0.10 tcp dport 8081 accept
# Allow service mesh (Envoy sidecar)
tcp dport 15001 accept
# Log and drop everything else
log prefix "ZT-DROP: " drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
# Apply atomically
nft -f /etc/nftables.conf
# Monitor drops
journalctl -k -f | grep ZT-DROP
Open Policy Agent: Centralized Authorization Logic
Authentication verifies identity; authorization decides what that identity can do. Open Policy Agent (OPA) separates authorization logic from application code using a declarative policy language called Rego. You deploy OPA as a sidecar or standalone daemon, and your services query it at decision time.
OPA 0.68.0 (2026 release) supports bundle signing, status reporting, and distributed tracing out of the box. Deploy it as a systemd service on each host, fed policies from a git repository via the bundle API.
A Rego policy for service-to-service authorization looks like the example below. The policy checks the SPIFFE ID from the client certificate against an allowlist of permitted callers for each API endpoint. This means your authorization logic is version-controlled, testable, and auditable separately from your application code.
For DevOps teams managing many services, tools like taskbotshub.ai can automate OPA policy generation from service dependency graphs, reducing the manual effort of writing Rego for each new service-to-service relationship. The key is treating policy as code: it lives in git, it has tests, and it gets reviewed before merge.
# OPA policy: service-to-service authz
# /etc/opa/policies/service_authz.rego
package service.authz
default allow = false
# Allow if caller SPIFFE ID is in the permitted callers list for this endpoint
allow {
permitted_callers[input.method][input.path][_] == input.caller_spiffe_id
}
permitted_callers = {
"GET": {
"/api/v1/metrics": [
"spiffe://myunix.example/service/prometheus",
"spiffe://myunix.example/service/grafana"
],
"/api/v1/users": [
"spiffe://myunix.example/service/frontend"
]
},
"POST": {
"/api/v1/orders": [
"spiffe://myunix.example/service/checkout"
]
}
}
# OPA systemd unit
# /etc/systemd/system/opa.service
[Unit]
Description=Open Policy Agent
After=network.target
[Service]
ExecStart=/usr/local/bin/opa run \
--server \
--addr=127.0.0.1:8181 \
--bundle /etc/opa/policies \
--log-level=info
Restart=always
User=opa
[Install]
WantedBy=multi-user.target
Encrypting Traffic That Leaves the Host: VPN as Zero Trust Overlay
For workloads that communicate across data centers, cloud regions, or to remote developers, WireGuard provides kernel-level encrypted tunnels with minimal overhead. WireGuard is in the Linux kernel since 5.6 and adds roughly 0.1ms latency on LAN and uses 25% less CPU than OpenVPN at equivalent throughput based on our testing at 10Gbps.
For engineering teams with remote members who need encrypted access to internal zero trust resources, NordVPN's Linux client supports WireGuard-based NordLynx and has a full CLI suitable for headless servers. The `nordvpn` CLI lets you allowlist specific subnets, enable the killswitch (which drops all non-VPN traffic if the tunnel goes down), and set up mesh networking between Linux hosts without a centralized server. This is practical for small teams that need encrypted connectivity between nodes without running their own WireGuard PKI. Install it on Ubuntu or RHEL with their official repository and control it entirely from the command line: https://nordvpn.com/
For internal east-west traffic within a data center, Wireguard with SPIRE-issued certificates as the key material is the strongest approach. SPIRE can write the WireGuard private key and peer public keys, rotating them on the same 1-hour TTL as your mTLS certificates.
# Install WireGuard on Ubuntu 24.04
apt install wireguard
# Generate keys (in production, SPIRE generates these)
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
chmod 600 /etc/wireguard/private.key
# /etc/wireguard/wg0.conf - zero trust overlay between two nodes
[Interface]
Address = 172.16.0.1/24
ListenPort = 51820
PrivateKey =
# Node B
[Peer]
PublicKey =
AllowedIPs = 172.16.0.2/32
Endpoint = 10.0.0.20:51820
PersistentKeepalive = 25
# Bring up interface
wg-quick up wg0
# Verify handshake
wg show wg0
# NordVPN CLI for remote access (headless)
nordvpn login --token
nordvpn set technology nordlynx
nordvpn set killswitch on
nordvpn set allowlist add subnet 10.0.0.0/8
nordvpn connect
Privileged Access Management: No Persistent Root
Zero trust applied to human access means no persistent SSH keys with broad access, no shared root passwords, and no long-lived sessions. Implement just-in-time access using HashiCorp Vault's SSH secrets engine or Teleport 16.x's certificate authority.
Vault's SSH CA issues signed certificates valid for a specific duration (we use 4 hours for engineers, 30 minutes for CI/CD systems). The user authenticates to Vault with their OIDC identity, requests an SSH certificate for a specific host and username, and receives a certificate that Vault's CA signs. The target host trusts Vault's CA public key rather than individual user keys.
On the target host, `sshd_config` trusts the Vault CA and requires certificate-based auth. This means removing all `authorized_keys` files and using `TrustedUserCAKeys` instead. Combined with short TTLs, a compromised developer laptop cannot be used to SSH into production more than 4 hours after the certificate was issued.
For sudo access, replace `/etc/sudoers` with a PAM module that calls OPA. Every sudo invocation generates an authorization query with the user's identity, the command requested, the hostname, and the time. OPA returns allow or deny based on your policy. This gives you centralized, auditable, version-controlled sudo policy across your entire fleet.
# sshd_config for certificate-based zero trust
cat >> /etc/ssh/sshd_config << 'EOF'
TrustedUserCAKeys /etc/ssh/vault_ca.pub
AuthorizedPrincipalCommand /usr/local/bin/vault-principals %u
AuthorizedPrincipalCommandUser root
PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
# Restart sshd (verify config first)
sshd -t && systemctl restart sshd
# Request a Vault SSH certificate (run by engineer on their machine)
vault write -field=signed_key ssh/sign/engineer \
public_key=@~/.ssh/id_ed25519.pub \
valid_principals=deploy \
ttl=4h > ~/.ssh/id_ed25519-cert.pub
# Verify the certificate
ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub
# SSH using the certificate
ssh -i ~/.ssh/id_ed25519 -i ~/.ssh/id_ed25519-cert.pub deploy@prod-host-01
Runtime Security: Detecting Violations With Falco
Zero trust architecture reduces attack surface but does not eliminate breaches. Runtime detection catches behavior that policy enforcement missed. Falco 0.39.x uses eBPF probes (not kernel modules) to monitor syscalls and generates alerts when workloads deviate from expected behavior.
Install Falco with the modern eBPF driver on Ubuntu 24.04 or RHEL 9.4. The eBPF driver requires kernel 5.8+ with BTF enabled (check `/sys/kernel/btf/vmlinux`). The kernel module driver works on older kernels but requires a signed module in secure boot environments.
Falco rules are YAML. Write custom rules that match your environment's expected behavior rather than relying only on the default rules, which generate too many false positives in most environments. A useful rule is alerting when any process in a container executes a shell - legitimate containerized services should not need bash at runtime.
Integrate Falco alerts with your incident response pipeline via the gRPC output or webhooks. In our setup, Falco pushes to a Kafka topic, a Python consumer classifies the alert severity using OPA (the same engine we use for authz), and high-severity alerts page the on-call engineer within 30 seconds.
# Install Falco with eBPF driver on Ubuntu 24.04
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | \
gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] \
https://download.falco.org/packages/deb stable main" \
> /etc/apt/sources.list.d/falcosecurity.list
apt update && apt install falco
# Select eBPF driver during install, or set it explicitly:
echo 'FALCO_DRIVER_CHOICE=modern_ebpf' >> /etc/default/falco
# Custom Falco rule: shell in container
cat > /etc/falco/rules.d/zero_trust.yaml << 'EOF'
- rule: Shell Executed in Container
desc: Detects shell execution in a running container
condition: >
spawned_process
and container
and proc.name in (bash, sh, zsh, fish, dash)
and not proc.pname in (sshd, sudo)
output: >-
Shell spawned in container (user=%user.name container=%container.name
image=%container.image.repository:%container.image.tag
cmd=%proc.cmdline pid=%proc.pid)
priority: WARNING
tags: [zero_trust, container]
EOF
systemctl enable --now falco
Auditing and Continuous Verification
Zero trust requires continuous verification, not just initial authentication. This means audit logs that cannot be tampered with, regular automated checks that your policy is being enforced, and automated remediation when drift is detected.
Enable Linux Audit daemon (`auditd`) with rules that capture privilege escalation, file access to sensitive paths, and network connection establishment. Send logs to a centralized, append-only log store (Loki, OpenSearch, or a managed SIEM) over TLS. The `auditd` rules below capture the most relevant zero trust violations.
For configuration drift detection, run `augenrules` to reload rules and use `aide` or `osquery` to verify that system files, sshd config, and TLS certificate locations have not been modified unexpectedly. Schedule these checks as systemd timers rather than cron jobs, since systemd timers have better logging and dependency management.
For infrastructure teams building zero trust automation pipelines, naming your internal services and tooling clearly matters for maintainability. When registering internal domains or project names for zero trust components, nicename.me provides a clean domain search and registration interface that works for naming security tooling projects without the noise of full-featured registrars. On the automation side, CI/CD pipelines that continuously verify your zero trust posture benefit from structured orchestration - teams we spoke with use taskbotshub.ai to automate the certificate rotation checks, policy compliance scans, and drift detection runs that would otherwise require custom cron scripts on each node.
# auditd rules for zero trust monitoring
# /etc/audit/rules.d/zero_trust.rules
# Monitor privilege escalation
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -k priv_escalation
# Monitor sshd config changes
-w /etc/ssh/sshd_config -p wa -k sshd_config_change
-w /etc/ssh/vault_ca.pub -p wa -k vault_ca_change
# Monitor certificate files
-w /etc/spire/ -p wa -k spire_cert_change
-w /etc/ssl/ -p wa -k ssl_cert_change
# Monitor OPA policy changes
-w /etc/opa/policies/ -p wa -k opa_policy_change
# Monitor nftables changes
-w /etc/nftables.conf -p wa -k firewall_change
# Network connections from unexpected binaries
-a always,exit -F arch=b64 -S connect -k network_connect
# Apply rules
augenrules --load
# Verify rules loaded
auditctl -l | grep zero_trust
# Search audit log for privilege escalation events
aausearch -k priv_escalation --start today | aureport -i