Skip to main content
Security Fundamentals · lesson 2 of 8
Security Fundamentals beginner Lesson 2 of 8

The CIA Triad, Demonstrated on One Machine

See the CIA triad fail and then hold: a sha256sum mismatch, Permission denied on a 0600 secret, and a capped server refusing a third client.

By SkillByExample · Published

security cia-triad integrity confidentiality availability

Confidentiality, integrity and availability, the CIA triad, are usually taught as a slide. Here each one fails on a machine you control, and then a small control makes it hold. The shell examples run as root in a throwaway container, because they create users. Start one with docker run --rm -it -v "$PWD":/work -w /work python:3.12-slim bash and run each script inside it. They were run with Python 3.12.15 on Debian 13.7, coreutils 9.7 and Docker Engine 29.8.2.

Integrity: a one-byte change breaks the checksum

Integrity means data isn’t changed without someone noticing. A SHA-256 checksum records what the file looked like, and sha256sum -c checks it later.

# integrity.sh
echo "pay alice amount=100" > payment.txt
sha256sum payment.txt > payment.sha256
sha256sum -c payment.sha256

sed -i 's/100/900/' payment.txt        # change one byte: 1 -> 9
cat payment.txt
sha256sum -c payment.sha256
echo "exit code: $?"
$ sh integrity.sh
payment.txt: OK
pay alice amount=900
payment.txt: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
exit code: 1

Changing a single byte produces a completely different digest, so the check fails, and it fails with a non-zero exit code that a deploy script or cron job can act on. Two limits matter, though. The checksum only detects the change; it doesn’t prevent it or restore the original. And anyone who can rewrite payment.txt and payment.sha256 together can make the check pass again. The first practice task fixes that with a key.

Confidentiality: the default permissions leak the secret

Confidentiality means only the right people can read the data. Here alice owns an application secret and bob is another account on the same machine.

# confidentiality.sh
useradd alice && useradd bob
echo "DB_PASSWORD=FAKE-example-not-real" > /srv/app.env
chown alice /srv/app.env

chmod 644 /srv/app.env                 # the default for a new file
su bob -c 'cat /srv/app.env'

chmod 600 /srv/app.env                 # owner only
su bob -c 'cat /srv/app.env'
su alice -c 'cat /srv/app.env'
ls -l /srv/app.env
$ sh confidentiality.sh
DB_PASSWORD=FAKE-example-not-real
cat: /srv/app.env: Permission denied
DB_PASSWORD=FAKE-example-not-real
-rw------- 1 alice root 34 Oct 13 18:13 /srv/app.env

The first line is the failure: that’s bob reading the secret. With mode 644, the usual result of a default umask of 022, he reads the password without any special privilege. Every other account on the host can too, including any service account an attacker might compromise. After chmod 600, the second line shows the kernel refusing bob, and the third shows it still letting alice read the file. Nothing was encrypted: the operating system’s access control did all the work. Least privilege at the file level is taught in depth in Linux Users and Permissions.

Availability: one idle client stalls a server

Availability means the service keeps answering the people entitled to use it. This server handles one client at a time and waits as long as it takes for a request.

# availability_bad.py
import socket, threading, time

srv = socket.create_server(("127.0.0.1", 9000))

def serve():                       # one client at a time, no timeout
    while True:
        conn, _ = srv.accept()
        conn.recv(100)             # waits for the client to send a request
        conn.sendall(b"OK\n")
        conn.close()

threading.Thread(target=serve, daemon=True).start()

idle = socket.create_connection(("127.0.0.1", 9000))   # connects, never sends
time.sleep(0.2)
c = socket.create_connection(("127.0.0.1", 9000), timeout=2)
c.sendall(b"GET\n")
try:
    print("second client got:", c.recv(100))
except TimeoutError as e:
    print("second client:", e)
$ python availability_bad.py
second client: timed out

The second client did nothing wrong. Its connection was accepted by the kernel, its request was sent, and then it waited two seconds for an answer that never came, because the only handler is stuck in recv waiting on the idle client. No data was stolen or changed, yet the service failed: that’s an availability loss. One connection that never speaks was enough.

Availability: a connection cap and a read timeout

The fix makes three changes. Each connection gets its own handler thread, so one slow client no longer blocks the accept loop. A cap allows at most two connections at a time and refuses the next one fast. A one-second read timeout frees the slot of a client that goes quiet. The cap only matters because of the threads: a single handler already serves one client at a time.

# availability_cap.py
import socket, threading, time

srv = socket.create_server(("127.0.0.1", 9000))
slots = threading.BoundedSemaphore(2)         # at most 2 clients at once

def handle(conn):
    conn.sendall(b"OK\n")
    conn.settimeout(1)                        # idle clients are dropped after 1 s
    try: conn.recv(100)
    except TimeoutError: pass
    finally: conn.close(); slots.release()

def serve():
    while True:
        conn, _ = srv.accept()
        if not slots.acquire(blocking=False):
            conn.sendall(b"BUSY\n"); conn.close(); continue
        threading.Thread(target=handle, args=(conn,)).start()

threading.Thread(target=serve, daemon=True).start()
start, clients = time.monotonic(), []
for i in range(1, 5):
    if i == 4: time.sleep(1.5)                # let the idle clients time out
    clients.append(socket.create_connection(("127.0.0.1", 9000), timeout=2))
    print(f"t={time.monotonic() - start:.1f}s client {i}:", clients[-1].recv(100))
$ python availability_cap.py
t=0.0s client 1: b'OK\n'
t=0.0s client 2: b'OK\n'
t=0.0s client 3: b'BUSY\n'
t=1.5s client 4: b'OK\n'

Clients 1 and 2 take both slots and then sit idle, just like the stalling client before. Client 3 is refused at once with BUSY instead of hanging. A fast “no” is better for availability than a slow nothing, because the caller can retry or fail over. After a second, the read timeout drops the two idle clients and releases their slots, so client 4 is served normally. Real servers apply the same two bounds: nginx has worker_connections and client_body_timeout, and PostgreSQL has max_connections and idle_session_timeout.

Practice

  1. Make the integrity check survive an attacker. Replace the plain checksum with an HMAC-SHA256 tag computed with a key the attacker doesn’t have. Show that a tampered message is rejected even when the attacker supplies a freshly computed SHA-256 of their own.
Solution
# hmac_check.py
import hashlib, hmac

KEY = b"FAKE-demo-key-keep-real-keys-out-of-code"

def tag(data: bytes) -> str:
    return hmac.new(KEY, data, hashlib.sha256).hexdigest()

original = b"pay alice amount=100"
stored_tag = tag(original)

tampered = b"pay alice amount=900"
forged_tag = hashlib.sha256(tampered).hexdigest()    # attacker has no key

for data, t in [(original, stored_tag), (tampered, stored_tag), (tampered, forged_tag)]:
    ok = hmac.compare_digest(tag(data), t)
    print(data.decode(), "->", "OK" if ok else "REJECTED")
$ python hmac_check.py
pay alice amount=100 -> OK
pay alice amount=900 -> REJECTED
pay alice amount=900 -> REJECTED

The attacker can compute a SHA-256 of anything, but not the HMAC, because that needs KEY. hmac.compare_digest compares in constant time, so response timing doesn’t reveal how much of a guessed tag was right. In a real system the key comes from a secret store, not from source code.

  1. Share the secret with one group, not everyone. Let members of a deploy group read app.env, keep everyone else out, and prove it with one member (bob) and one non-member (carol).
Solution
# group_read.sh
useradd alice && useradd bob && useradd carol
groupadd deploy && usermod -aG deploy bob
echo "DB_PASSWORD=FAKE-example-not-real" > /srv/app.env
chown alice:deploy /srv/app.env
chmod 640 /srv/app.env
su bob -c 'cat /srv/app.env'
su carol -c 'cat /srv/app.env'
ls -l /srv/app.env
$ sh group_read.sh
DB_PASSWORD=FAKE-example-not-real
cat: /srv/app.env: Permission denied
-rw-r----- 1 alice deploy 34 Oct 13 17:54 /srv/app.env

Mode 640 is owner read-write, group read, others nothing. Granting access through a group, not by widening “others”, keeps the list of readers explicit: the members of deploy.

  1. Find what a checksum manifest misses. Record checksums for every .conf file in a directory, then add a new .conf file instead of editing one. Does sha256sum -c notice? Add a check that does.
Solution
# manifest.sh
mkdir -p conf && cd conf
echo "port=8080" > app.conf
echo "level=info" > log.conf
sha256sum *.conf > ../manifest.sha256

echo "debug=true" > backdoor.conf     # a new file, not an edit
sha256sum -c ../manifest.sha256
ls *.conf | sort > /tmp/now
cut -d' ' -f3 ../manifest.sha256 | sort > /tmp/known
comm -13 /tmp/known /tmp/now | sed 's/^/NOT IN MANIFEST: /'
$ sh manifest.sh
app.conf: OK
log.conf: OK
NOT IN MANIFEST: backdoor.conf

sha256sum -c only checks the files listed in the manifest, so it reports a clean result while a new file sits next to them. An integrity check has to cover the full set of files as well as their contents. File integrity monitoring tools such as AIDE track both.

  1. Predict, then raise the cap. Change the semaphore in availability_cap.py to BoundedSemaphore(3). Before running it, predict what client 3 receives.
Solution
slots = threading.BoundedSemaphore(3)         # at most 3 clients at once
$ python availability_cap3.py
t=0.0s client 1: b'OK\n'
t=0.0s client 2: b'OK\n'
t=0.0s client 3: b'OK\n'
t=1.5s client 4: b'OK\n'

All four clients are served: three fit under the cap, and the fourth arrives after the timeouts have freed the slots. The cap is a capacity decision. Set it from what the server can really handle, or it either refuses legitimate users or fails to protect the server.

Frequently Asked Questions

What is the CIA triad in cybersecurity?
The CIA triad is the three properties security protects: confidentiality (only the right people can read data), integrity (data is not changed without detection), and availability (the system keeps serving the people who are entitled to use it). Almost every control maps to at least one of them, so the triad is a quick way to ask what a control actually protects.
Does a SHA-256 checksum protect file integrity against an attacker?
Only if the attacker cannot also change the stored checksum. A plain hash detects accidental corruption and unauthorised edits when the checksum lives somewhere the attacker cannot write. If both sit side by side, an attacker can edit the file and recompute the hash. A keyed HMAC or a digital signature closes that gap, because producing a valid tag needs a secret key.
Why is a file with permissions 644 a confidentiality problem?
Mode 644 means the owner can read and write, and every other user on the machine can read. That is the usual default for new files, so a secrets file created without thought is readable by every account and every compromised service on the host. Mode 600 limits reading to the owner, and 640 adds one group.
How do you protect availability against a slow or idle client?
Bound every resource a client can hold. Cap the number of concurrent connections, refuse new ones quickly when the cap is reached, and put a timeout on every read so an idle client releases its slot. A server with no cap and no timeout can be stalled by a single client that connects and never sends anything.
Which part of the CIA triad matters most?
It depends on the system. A payments ledger cares most about integrity, a medical record about confidentiality, and an emergency dispatch service about availability. The triad does not rank the three; it makes you decide, per asset, which loss would hurt most, which is where risk assessment starts.