Encrypting Files and Connections

eCryptfs, mutual TLS with OpenSSL and NFS wrapped in stunnel.

Contents
  1. Background: three questions
  2. Experiment 1: encrypting a directory with eCryptfs
    1. How it works
    2. Doing it
    3. Checking it
  3. Experiment 2: a private CA and mutual TLS
    1. Background: certificates and CAs
    2. Building the CA and certificates
    3. Connecting with mutual authentication
  4. Experiment 3: wrapping NFS in TLS with stunnel
    1. The problem
    2. Setting it up
    3. Checking it
  5. What the three have in common
  6. Further reading

Encryption usually reaches us as a setting: a checkbox for disk encryption, a padlock in the browser, a tls: true in a config file. I wanted to see what each of those actually does, so I set up three small experiments on a couple of VMs, each protecting data in a different place:

  1. At rest, sitting in a file on disk.
  2. In transit, between two programs that have to prove who they are to each other.
  3. Underneath a protocol that has no encryption of its own, in this case NFS.

None of the tools here are exotic. They’re the standard ones: eCryptfs, the OpenSSL command line and stunnel. The point was to use them by hand and then check the raw bytes to confirm each one really worked.

Background: three questions#

Each experiment answers a different question.

Question Threat it handles
At rest Can someone with the disk read my files? A stolen laptop, a disk pulled from a server, a leaked backup
In transit Can someone on the network read or change my traffic? And is the other end who it says it is? Anyone between the two machines
Wrapping a protocol Can I protect a protocol that sends everything in plaintext, without changing it? The same as in transit, for software you can’t modify

Two ideas come up throughout:

  • Symmetric encryption uses one shared key to both encrypt and decrypt. It’s fast and it’s what actually protects the data in all three experiments.
  • Public-key cryptography gives each party a key pair: a private key they keep and a public key anyone can have. It solves the question of who you’re talking to and it’s how two sides agree on a symmetric key without ever sending it in the clear.

Experiment 1: encrypting a directory with eCryptfs#

How it works#

eCryptfs is a stacked filesystem. You mount it on top of an ordinary directory and everything written through the mount is encrypted before it reaches the directory underneath. Read it back through the mount and it’s decrypted on the fly. Programs using the mounted path never notice.

Both the contents of each file and its name are encrypted, using a key derived from a passphrase you supply when mounting.

Through the mount plaintext, readable secret.txt: hello eCryptfs stacked filesystem encrypts on write passphrase Raw disk, unmounted ciphertext only e3f9c1: 9f 3a c1 e2 The plaintext exists only while the mount is live. On disk, filename and contents are both encrypted.
Plaintext only exists above the mount. The disk only ever holds ciphertext.

Doing it#

Mount a directory on top of itself. eCryptfs asks a few questions, including the passphrase and whether to encrypt filenames:

shell
mkdir -p ~/vault
sudo mount -t ecryptfs ~/vault ~/vault
# choose a passphrase, aes, and "y" to filename encryption when prompted

echo "the launch code is 0000" > ~/vault/notes.txt
cat ~/vault/notes.txt       # readable, because we're reading through the mount
sudo umount ~/vault

Checking it#

The test that makes it real is looking at the file with the mount gone:

shell
ls ~/vault                  # the filename is now a long encrypted string
cat ~/vault/ECRYPTFS_FNEK_ENCRYPTED.*   # the contents are ciphertext

With eCryptfs unmounted, both the name and the contents are unreadable. Mount it again with the same passphrase and cat prints the plaintext straight back. The plaintext only exists while the mount is live. On disk, there is nothing but encrypted bytes.

eCryptfs is effectively unmaintained now and Ubuntu has dropped it from its installer. For new setups, full-disk encryption with LUKS, or per-directory encryption with fscrypt or gocryptfs, are the better choices. The idea is the same.

Experiment 2: a private CA and mutual TLS#

Background: certificates and CAs#

A TLS certificate binds a public key to a name and it’s signed by a certificate authority (CA). If you trust the CA, you can trust any certificate it has signed. Browsers ship with a list of public CAs they trust, but nothing stops you from creating your own CA and trusting only that.

Normal HTTPS is one-way: the server proves its identity and the client stays anonymous. Mutual TLS (mTLS) makes the client present a certificate as well, so each side has to prove who it is before any data flows. It’s widely used between services and for locking down admin interfaces.

Private CA self-signed root signs signs openssl s_server server cert requires client cert openssl s_client client cert presents its cert each verifies the other back to the CA
One private CA signs both certificates and each side verifies the other against it.

Building the CA and certificates#

A CA is just a key pair and a certificate that signs itself:

shell
# 1. The CA: a private key and a self-signed root certificate
openssl req -x509 -newkey rsa:4096 -nodes -days 365 \
  -keyout ca.key -out ca.crt -subj "/CN=Lab Root CA"

# 2. A server key and a certificate signing request (CSR)
openssl req -newkey rsa:2048 -nodes \
  -keyout server.key -out server.csr -subj "/CN=server.lab"

# 3. The CA signs the server's request
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 365 -out server.crt

# 4. Same again for the client
openssl req -newkey rsa:2048 -nodes \
  -keyout client.key -out client.csr -subj "/CN=client.lab"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 365 -out client.crt

Once ca.key exists it can sign anything, which is why real CAs keep that key offline and heavily guarded.

Connecting with mutual authentication#

openssl s_server and openssl s_client are enough to stand up a TLS server and connect to it. -Verify 1 (capital V) tells the server to require a client certificate, not just ask for one:

shell
# Server: present server.crt, require a client certificate signed by our CA
openssl s_server -accept 4433 -cert server.crt -key server.key \
  -CAfile ca.crt -Verify 1

# Client: present client.crt, verify the server against our CA
openssl s_client -connect server.lab:4433 -cert client.crt -key client.key \
  -CAfile ca.crt

The debug output shows each side’s certificate chain being checked against the CA, in both directions. Leave out -cert on the client and the server refuses the handshake. Neither end trusts the other until the certificate checks out.

Adding -www to s_server makes it answer HTTP requests, so a browser can connect as well. Import the client certificate into the browser (as a PKCS#12 bundle made with openssl pkcs12 -export) and the browser prompts you to pick a certificate before the page loads. It’s the same client-certificate login that some real services use to gate access.

Experiment 3: wrapping NFS in TLS with stunnel#

The problem#

NFS lets one machine mount a directory from another over the network. Out of the box it sends file contents in the clear. That’s acceptable on a trusted LAN and alarming anywhere else.

Instead of switching to a different protocol, you can tunnel it. stunnel is a small program that accepts plain TCP on one side and speaks TLS on the other. Put one at each end and the program in the middle never knows its traffic is encrypted.

NFS client mounts local end stunnel client end TLS stunnel server end NFS server exports local end NFS speaks cleartext to its local stunnel. Only encrypted TLS crosses the wire between the two stunnels.
NFS talks to stunnel locally and only TLS crosses the network.

Setting it up#

The trick is where each side points:

  • On the server, stunnel accepts TLS on a new port and forwards the decrypted traffic to the local NFS server. Because the NFS server now sees every connection coming from 127.0.0.1, it exports to localhost, not to the client’s address.
  • On the client, stunnel listens on a local port and the client mounts NFS from its own localhost. stunnel carries that connection across the network in TLS.

The configuration is short. On the server:

/etc/stunnel/nfs-server.conf
[nfs]
accept  = 20490
connect = 127.0.0.1:2049
cert    = /etc/stunnel/server.crt
key     = /etc/stunnel/server.key

On the client, with the server’s certificate copied over so it can verify it:

/etc/stunnel/nfs-client.conf
[nfs]
client  = yes
accept  = 127.0.0.1:2049
connect = <server-ip>:20490
CAfile  = /etc/stunnel/server.crt
verify  = 2

Then mount through the local end of the tunnel:

shell
sudo mount -t nfs4 127.0.0.1:/export /mnt/remote

Checking it#

A packet capture on the link between the two machines tells the whole story. Before the tunnel, the same mount showed readable NFS operations in the capture, including file contents. With the tunnel up, the capture shows nothing but a TLS handshake followed by encrypted records.

Newer Linux kernels can do this natively. RPC-with-TLS lets NFS use TLS directly through the xprtsec=tls mount option and the tlshd daemon from ktls-utils. The stunnel approach still works everywhere and it shows the idea more clearly.

What the three have in common#

Tool What protects the data How trust is established
At rest eCryptfs A symmetric key derived from a passphrase Knowing the passphrase
In transit OpenSSL, mTLS A session key agreed during the TLS handshake Certificates signed by a CA both sides trust
Wrapped protocol stunnel The same TLS session keys The server’s certificate, pinned on the client

All three answer the same two questions: who can read this data and who am I trusting? Doing each one by hand, then capturing or dumping the raw bytes to confirm it worked, is what turned them from settings I switch on into mechanisms I understand.

Further reading#