Layer Defenses Around a Local Secret Service

CybersecurityBeginner
Practice Now

Introduction

Defense in depth means using several controls together so one mistake does not expose an important asset by itself. A token check, a local network binding, file permissions, and logging each protect a different part of the same service.

In this lab, you will harden a tiny Python service that reads a secret file. You will make one small change at a time, run the service, test it with curl, and verify that the layers work together.

Start from an Exposed Secret

In this step, you will inspect the service and observe the first control: a request without the expected token is rejected, while a request with the token succeeds. Defense in depth starts by checking what is already protected and what still needs another layer.

Move into the project directory:

cd /home/labex/project

List the prepared files:

ls -l

You should see secret_service.py, secret.txt, and service-check.py.

Start the service in the background. The & at the end tells the shell to keep the service running while you continue typing commands:

python3 secret_service.py > service.log 2>&1 &

Save the background process ID so you can stop it cleanly later:

echo $! > service.pid

Try to read the secret without a token:

curl -i http://127.0.0.1:8081/secret

The response should include 403 Forbidden. Now send the required token header:

curl -s -H "X-Access-Token: lab-token-2026" http://127.0.0.1:8081/secret

You should see:

Project launch code: ORBIT-742

Stop the service before moving on:

kill "$(cat service.pid)"

Run the checker for the token layer:

python3 service-check.py token

The expected result is:

token: complete

Keep the Service Local

In this step, you will confirm the network exposure layer. A service that only needs local access should bind to 127.0.0.1, also called localhost. That address accepts connections from the same machine only.

Inspect the service default host setting:

grep 'default="127.0.0.1"' secret_service.py

You should see the argument definition for the --host option. This default means that running python3 secret_service.py starts the service on localhost.

Start the service again:

python3 secret_service.py > service.log 2>&1 &

Save the process ID:

echo $! > service.pid

Use ss to inspect listening TCP sockets. The -ltn options mean listening, TCP, and numeric addresses:

ss -ltn | grep ':8081'

The output should show 127.0.0.1:8081, which is evidence that the service is local-only:

LISTEN ... 127.0.0.1:8081 ...

Stop the service:

kill "$(cat service.pid)"

Run the checker for the localhost layer:

python3 service-check.py localhost

The expected result is:

localhost: complete

Restrict the Secret File

In this step, you will add a filesystem permission layer. Even if the service has a bug, the secret file should not be readable by every local account.

Check the current file permissions:

ls -l secret.txt

The file starts with broader permissions so you can improve it. Use chmod 600 to make the file readable and writable only by its owner:

chmod 600 secret.txt

Check the permissions again:

ls -l secret.txt

The permission bits should now start with -rw-------:

-rw------- ... secret.txt

Run the checker for this layer:

python3 service-check.py permissions

The expected result is:

permissions: complete

Log Rejected Requests

In this step, you will test the detection layer. Blocking an unauthorized request is useful, but recording that rejection gives operators evidence that someone tried to access the protected route.

Remove any old log file so this step starts cleanly:

rm -f access.log

Start the service:

python3 secret_service.py > service.log 2>&1 &

Save the process ID:

echo $! > service.pid

Send a request without the token:

curl -s http://127.0.0.1:8081/secret

The response body should be:

forbidden

View the access log:

cat access.log

The log should contain rejected, /secret, and missing_or_bad_token.

Stop the service:

kill "$(cat service.pid)"

Run the checker for the logging layer:

python3 service-check.py logging

The expected result is:

logging: complete

Verify the Layers Together

In this step, you will verify the full defense-in-depth result. The service should require the token, stay local by default, protect the secret file with owner-only permissions, and log rejected requests.

Run the full checker:

python3 service-check.py all

Every layer should be complete:

token: complete
localhost: complete
permissions: complete
logging: complete

Start the service one final time:

python3 secret_service.py > service.log 2>&1 &

Save the process ID:

echo $! > service.pid

Confirm that the secret still works for an authorized request:

curl -s -H "X-Access-Token: lab-token-2026" http://127.0.0.1:8081/secret

You should still see:

Project launch code: ORBIT-742

Stop the service:

kill "$(cat service.pid)"

This final check matters because defense in depth should protect the service without breaking the intended authorized use.

Summary

You practiced defense in depth by layering controls around one small service. You verified a token requirement, local-only binding, owner-only file permissions, and logging for rejected access.

The main lesson is that each layer protects a different failure point while the authorized service behavior still works.