Production Security And Server Hardening
Checksums, Signatures, And File Integrity
Verifying What You Download
For a downloaded artifact — a phar tool, an installer, a release tarball — verification looks like:
sha256sum phpunit.phar # compare against the published digest
gpg --verify phpunit.phar.asc phpunit.phar # verify the maintainer's signature
The checksum catches corruption and mismatched versions. The GPG signature ties the artifact to a maintainer key, which is only as strong as how you obtained that key: fetched over the same channel as the download it proves nothing; obtained out-of-band or pinned from a previous verified release it proves authorship. In practice, most day-to-day supply-chain protection in PHP comes from infrastructure that does this invisibly: apt verifies repository signatures on every install, and Composer records a hash for every package in composer.lock, refusing archives that do not match. That is why the lock file is committed and why composer install (which obeys the lock) belongs in deploys while composer update does not.
Integrity Of The Running System
The second use of integrity is detective: knowing when files that should not change, changed. The deployed codebase is the highest-value target — a webshell, a trojaned vendor file, an edited config. Because deploys are the legitimate change mechanism, they are also the natural checkpoint: record a manifest of file hashes at deploy time, then periodically hash the live tree and diff. Any difference is either a broken deploy process or an intruder, and both deserve an alert. The comparison must exclude the genuinely mutable paths (storage, uploads, caches) — which is one more reason the least-privilege lesson keeps those paths from being executable.
<?php
declare(strict_types=1);
$manifest = ['index.php' => hash('sha256', '<?php echo "v1";')];
$live = hash('sha256', '<?php echo "v1";');
echo $manifest['index.php'] === $live ? 'unchanged' : 'TAMPERED';
echo PHP_EOL;
// Prints:
// unchanged
For system files, host-based tools (AIDE and kin) maintain the same baseline-and-diff over /etc, binaries, and cron. Their weakness is operational, not cryptographic: a baseline that is updated carelessly after every change trains operators to approve diffs blindly. Tie baseline updates to the change process — a diff is expected when a deploy or patch ran, and alarming otherwise.
One structural caveat: an integrity checker running on a compromised host can itself be tampered with. Storing manifests off-host and comparing from outside raises the bar from "attacker edits files" to "attacker subverts the comparison pipeline," which is the meaningful step.
What To Check
Before moving on, make sure you can:
- state precisely what a checksum proves and what only a signature can prove
- explain why a checksum hosted beside its file adds nothing against tampering
- describe how apt and composer.lock verify integrity invisibly
- design deploy-time manifests and scheduled diffs for the codebase
- explain why manifests belong off-host
What You Should Be Able To Do
After this lesson, you should be able to verify downloaded artifacts with the right tool for the threat, keep dependency integrity anchored in the lock file, and run file-integrity monitoring that alerts on real tampering without drowning operators in deploy noise.
Practice
Practice: Design Integrity Coverage
Show solution
Dependencies: composer.lock is committed; CI and deploys run composer install only, so every package hash is enforced against the lock. composer update happens in reviewed pull requests where the lock diff is visible. That makes the lock file the integrity anchor, and its review the human checkpoint.
Deployed code: the deploy pipeline writes a manifest of sha256 hashes for the released tree and ships it off-host with the deploy record. A scheduled job hashes the live tree and compares from outside the server, excluding storage and uploads. Differences alert with the file list; the only expected differences coincide with deploy timestamps.
System files: AIDE over /etc, binaries, and cron baselines, with baseline updates allowed only inside the change process.
Each layer's records live off the host they protect — pinned digests in git, deploy manifests in the artifact store — because integrity records stored next to the thing they verify share its fate.
Practice: Diagnose A Verification Theater
A team "verifies" a CLI tool in CI by downloading tool.phar and tool.phar.sha256 from the vendor site in the same job, comparing them, and proceeding. One day the vendor site is compromised; the build keeps passing and ships the malicious tool.
Task
Explain why the check provided no protection, and give the fix hierarchy from cheapest to strongest.
Show solution
The check compared the attacker's file against the attacker's checksum. Both artifacts came from the same compromised origin in the same job, so agreement between them was guaranteed — the comparison verified transfer integrity and nothing more. Any tampering scenario that can alter the file can alter its neighboring digest.
Fix hierarchy:
- Cheapest: pin the digest in your own repository. The expected sha256 is committed after a one-time verification; CI compares the download against the pinned value. The vendor compromise now fails the build, and updating the tool requires a reviewed commit that changes the pin — a human checkpoint exactly where one belongs.
- Stronger: verify the maintainer's GPG signature with a pinned public key. This survives even a compromised download mirror and detects a compromised release process, provided the key was obtained out-of-band and is not refetched per build.
- Strongest for this class: remove the ad-hoc download. If the tool is installable through Composer, the lock file's hash enforcement covers it with infrastructure you already trust and review.
The general test for any verification step: ask what the attacker controls in your threat model, and whether the check still binds when they control it. A check that passes under full attacker control of the origin is theater.
Practice: Define Integrity Review Checks
Show solution
- CI fails if any build step downloads an artifact without comparing against a repository-pinned digest — enforced by convention lint on the pipeline files, not memory.
- A check asserts deploys run
composer installwith the lock present and thatcomposer.lockis never gitignored; a second check alerts when a lock diff merges without review approval. - The off-host integrity comparison runs on schedule, and its "last clean comparison" timestamp is monitored — a dead integrity job looks exactly like a clean one unless freshness is asserted.
- Every integrity alert maps to a deploy record within its time window automatically; unmatched differences page.
Human, quarterly:
- Sample one pinned digest and one pinned key: re-derive them from the vendor's current release out-of-band and confirm they still match, catching silent rot or forgotten rotations.
- Review AIDE baseline updates from the quarter against change tickets; baseline updates without a corresponding change are the drill's finding.
- Run one tabletop: "integrity diff shows an unexpected vendor/ file at 03:00" — walk who is paged and what happens next, connecting this lesson to the response playbooks.