Releases: fidpa/ubuntu-server-security
Release list
v1.2.0: install.sh runs the installers it could not find, and the README says what is not there
install.sh derived each installer's file name from the component name. Three
components ship a script the derivation never produced, a fourth was called
without the argument it requires, and the exit status of every installer was
discarded, so a run that did nothing still ended in [SUCCESS]. All four are
fixed, and the README was measured against the tree in the same pass.
Fixed
install.shfinds the installers ofboot-security,kernel-hardening
andlynisagain. The script derived the file name from the component
name (setup-${component//-/_}.sh), but the scripts are named after what
they configure:setup-grub-password.sh,setup-kernel-hardening.shwith
hyphens,install-lynis.sh. No name matched, and the three components fell
through to the manual path. A lookup table now maps component to installer,
so a renamed script fails loudly instead of disappearing. Automated
components go from four to seven.install.sh auditddeploys rules instead of printing its usage.
deploy-auditd.shrequires a mode (base,aggressive,docker) and
exited 1 without one. The table carries the argument, andbaseis the
documented recommendation.- A failing installer is reported as a failure. The exit status of the
component script was discarded: every run ended in[SUCCESS] Installation complete!, including theauditdrun that had just refused to do anything.
install_componentnow propagates the status andmainaborts on it. --dry-runuses the installer's own dry run where one exists. Both
deploy-fail2ban.shandinstall-lynis.shimplement--dry-runand list
the files they would write;install.shprinted "Would execute" and never
called them. It now passes the flag through when running as root, and says
so where it cannot, since both scripts check for root before doing anything.--listmarks each componentscriptormanual, derived from the same
table the installation uses, so the list cannot drift from the behaviour.check_prerequisitesno longer prints/etc/os-release: line 4: VERSION: readonly variable. It sourced/etc/os-release, whoseVERSIONcollides
with the script's own readonlyVERSION; it now readsIDand
PRETTY_NAMEout of the file without sourcing it.
Changed
- The README states what the repository does and what it does not. Every
claim was measured against the tree. Corrected:apparmor/ships one profile
(PostgreSQL 16); Docker keeps its owndocker-default, which this repository
does not provide.install.shautomates seven of the fourteen components and
points the other seven at theirdocs/SETUP.md; that is now stated instead
of a Quick Start that implied full automation. The SSH tally reads 15 of 18
applicable CIS 5.2.x controls with the benchmark version, per
ssh-hardening/docs/CIS_CONTROLS.md, in place of "15+". The AIDE figure
carries its measurement (3,799 to 12 changes per day). The drop-in example
names99-custom.conf.example, the file that exists. - Removed: the unsourced numbers. The "CIS Benchmark 100%" badge, "40+
controls" and "running on multiple servers with 100% CIS compliance" were
claims about servers outside this repository and could not be checked from
it. Compatibility now separates tested (Ubuntu 22.04, 24.04) from untested
(Debian, Raspberry Pi OS), and a Known Limitations section names the lockout
risk, the missing idempotency and the limits of a CIS mapping. - Fixed: two dead links under "See Also".
ubuntu-server-security-ansible
does not exist, and the monitoring templates live at
fidpa/linux-monitoring-templates.
Upgrade notes
sudo ./install.sh auditd now deploys the CIS Level 1 audit rules
(deploy-auditd.sh base), where it previously printed the usage of that script
and changed nothing. A host that ran the command before and assumed it was a
no-op will gain /etc/audit/rules.d/ entries and a restarted auditd on the
next run. The rules log, they do not deny; deploy-auditd.sh backs up the
existing rules before writing, and ./install.sh --dry-run auditd names the
command it would run. To keep the old behaviour, do not call the component
through install.sh.
v1.1.5: GitHub identifies the project as MIT-licensed
Changed
- The repository page shows the MIT licence, and licence-filtered searches
find the project.LICENSEcarried the repository URL on its own line
under the copyright notice. GitHub reads a licence text with an extra line as
modified and reportsNOASSERTION, which leaves the licence field on the
repository page empty. The line is gone; the MIT text and the copyright
notice are byte-for-byte unchanged, and the URL is still inREADME.md.
v1.1.4: Release notes match the tags they describe, and the remaining scripts are executable
Editorial pass over every section of this file, plus two changes to the repository
itself. Each section now leads with what a release changed for an operator, and each
statement it makes about the code was checked against the tag it describes, using
git show <tag>:<path>. The release notes on GitHub are built from these sections, so
the two now say the same thing.
Nothing was invented and nothing was softened: every measured value, path, and function
name that held up stayed as it was. The corrections are listed below, each with the
place in the tree that settles it.
Fixed
install-lynis.shandvalidate-lynis-profile.shrun directly again, as their own
documentation describes.v1.1.3restored the executable bit on 14 scripts but left
21 others at100644, including these two, whichlynis/docs/SETUP.mdand
lynis/docs/CUSTOM_PROFILES.mdinvoke assudo ../scripts/<script>.sh. All 21 now
carry mode100755acrossaide/,auditd/,fail2ban/,lynis/,nftables/,
security-monitoring/,ufw/, andvaultwarden/. Content is unchanged, only the
file mode.vaultwarden/vaultwarden-credentials.shdeliberately keeps100644: it is
a library thatvaultwarden/README.mdtells you tosource, not to run.- The initial release section no longer credits
v1.0.0with a component set and an
installer that tag does not carry.security-monitoring/,usb-defense/, and
install.share absent fromgit ls-tree -r v1.0.0; they arrive withv1.0.1and
v1.1.0. The[1.0.0]section now says which parts of it the tag actually contains. - The fail2ban entry in
[1.0.0]states the number of jails the tag ships.
jail.local.templateand the four numbered files infail2ban/drop-ins/define six
jails between them; the section had claimed "15+", which is the count of SSH CIS
controls used elsewhere in this repository. - The defense-in-depth entry in
[1.0.0]counts layers, not components. Fourteen is
the number of components; the layer table in that tag'sREADME.mdhas nine rows, and
the table reached eleven withv1.0.1. - The
[1.1.2]entry on release notes no longer generalises over the commit history.
Two of the twelve commit messages in this repository are not bare version lines
(Remove marketing files (should not be public)and a merge commit), which is why the
entry now names the condition instead of asserting it holds for every commit. - The
[1.1.2]security entry names what was removed without describing what it
contained. The previous wording identified, file by file, the kind of value each
removal had carried. Those files remain reachable through the history, so the wording
was a route to exactly the data the release had set out to remove.
Changed
- A tagged release now arrives on GitHub with a headline instead of a bare version
number..github/workflows/release.ymlreads the headline from the matching
CHANGELOG.mdheading (## [X.Y.Z] - YYYY-MM-DD: <headline>) and passes it to
softprops/action-gh-releaseasname:. Without it the action falls back to the tag
name, which is whatv1.1.2andv1.1.3shipped with. The job logs a warning and
falls back to the plain tag when a heading carries no headline, rather than failing the
release. - The release body no longer starts with a blank line. Extraction moved from a
sed
range toawkanchored at the start of the line (index($0, head) == 1), followed by
sed -e '/./,$!d'. Thesedrange matched any heading containing the version string,
including one that merely mentions another version in its headline.
Upgrade notes
None required. No firewall rule, sshd option, sysctl value, fail2ban threshold, or
installation path changed. Scripts that were readable and runnable via bash <script>
are now also runnable via ./<script>; nothing that worked before stopped working.
Readers building against the v1.0.0 tag should note that it sits on the first of three
commits carrying that version message. compare/v1.0.0...v1.0.1 therefore shows two
whole components arriving in what its section describes as a documentation patch. The tag
is not being moved; v1.0.1 and later are unaffected.
v1.1.3: Component scripts run directly again
Fixed
- Fourteen component scripts could not be started with
./script.sh. They were
checked in as100644, so onlybash script.shworked, while the component READMEs
describe the direct call. The executable bit is restored acrossaide/,apparmor/,
auditd/,boot-security/,kernel-hardening/,nftables/,ssh-hardening/, and
ufw/. Content is unchanged, only the file mode.
v1.1.2: install.sh reports the repository version, and the docs carry no private identifiers
Housekeeping release. No hardening rule, default, or access path changes -- every
change below is documentation, tooling, or repository hygiene. It is safe to
apply on a running server, and it changes nothing on one.
Two release commits (1.1.0 and 1.1.1) were never tagged, so the last
published release was v1.0.1 from January 2026. This release supersedes both;
v1.1.0 and v1.1.1 will not receive tags of their own.
Added
install.sh --versionanswers "which hardening level is running on this server?"
correctly again. AVERSIONfile in the repository root is now the single source of
the project version, andinstall.shreads it at runtime, falling back tounknown
instead of failing. Until now the constant ininstall.shreported1.0.0while the
changelog stood at1.1.1, two releases of drift in the one number an operator quotes.- Bug reports and pull requests arrive in a usable shape.
.github/ISSUE_TEMPLATE/(bug report, feature request, config) and
.github/PULL_REQUEST_TEMPLATE.md.
Changed
- A tagged release now carries the changelog section instead of the commit subjects.
.github/workflows/release.ymlbuilds the release notes from the matching
CHANGELOG.mdsection instead ofgenerate_release_notes: true. Release commits in
this repository carry a barevX.Y.Zline as their subject, so generated notes said
nothing the tag did not already say. The job fails loudly when a tag has no usable
changelog section rather than publishing an empty release.
softprops/action-gh-releasemoved fromv1tov2. - The ShellCheck threshold now lives where it takes effect. The
severity=warning
line was removed from.shellcheckrc. ShellCheck has noseveritykey for that file
and discards unknown keys without a diagnostic, so the line looked like a setting and
did nothing. Measured across all 40 scripts: 49 messages with the line present, 49 with
a nonsense key in its place, 37 with--severity=warningon the command line. The
binding threshold lives inlint.yml(--severity=error) and is now documented as such. - fail2ban alerts identify the host they came from, on any host.
fail2ban/actions/telegram-send.sh(2.0.0 -> 2.0.1) no longer hardcodes two specific
hostnames in the alert prefix. It defaults to the short hostname, documents how to add
per-host cases, and now honours anALERT_PREFIXset in the secrets file; the previous
assignment was unconditional and would have discarded one.
Fixed
- Four relative documentation links resolve again.
aide/README.mdand
aide/docs/SETUP.mdpointed atPROMETHEUS_INTEGRATION.mdandFAILURE_ALERTING.md
inside the component directory, while both live in the repository-leveldocs/;
fail2ban/README.mdlinkedTELEGRAM_ALERTS.md, which is named
TELEGRAM_INTEGRATION.md. - The
1.1.1release is reachable from this file. It had no link definition and no
row in the version history table; both were added. The1.1.0and1.1.1link
definitions now point at their commits rather than at compare ranges between tags that
were never created, which returned 404.
Removed
- Two marketing drafts are no longer part of the published tree.
aide/LINKEDIN_POST.mdandaide/REDDIT_POST.mdwere tracked and therefore public.
.gitignorecarries matching entries, but ignore rules only apply to untracked files,
sogit check-ignorereports them as not ignored, which reads like a different
problem. The root-level copies were removed in January 2026; these two were missed.
Security
- The published documentation no longer carries identifiers from the author's own
infrastructure. None of it affected the hardening rules, and all of it was public.
Corrected in.shellcheckrc,fail2ban/actions/telegram-send.shand its
documentation,fail2ban/docs/GEOIP_FILTERING.md,
nftables/docs/WIREGUARD_INTEGRATION.md,ufw/docs/SETUP.md,
usb-defense/docs/THREE_LAYER_DEFENSE.md, and five files undernftables/. Examples
now use RFC 2606 names, the generic accountadmin, and a target derived at runtime;
attributions read "a production gateway config". Generic references to Raspberry Pi OS
as a supported platform are deliberately kept. - The remaining German documentation is readable to the audience this repository has.
Translated into English: the whole ofufw/docs/andufw/examples/,
ufw/drop-ins/README.md, and one code comment in
security-monitoring/scripts/security-log-monitor.sh. This is not cosmetic for a
security repository: a reader who cannot read a rule's rationale applies the rule
without understanding it.
Upgrade notes
None required. No firewall rule, sshd option, sysctl value, fail2ban threshold,
or installation path changed. install.sh --version now reports the repository
version instead of 1.0.0; if any tooling parsed that constant, it will now see
1.1.2.
v1.0.1: Contribution and vulnerability reporting paths
Changed
- Abuse reports reach a channel that is actually monitored.
CODE_OF_CONDUCT.mdmoved the contact method from email to GitHub Issues.
Added
- A contributor knows the expected shape of a change before opening one.
CONTRIBUTING.mdwith development guidelines. - The project states which behaviour it expects.
CODE_OF_CONDUCT.md
(Contributor Covenant v2.1). - A security finding has a documented, non-public route in.
SECURITY.mdwith the
vulnerability reporting process.
v1.0.0: Initial release, aligned with the CIS Ubuntu Benchmark
First public release. A server operator can harden boot, kernel, network, access
control, and audit paths on Ubuntu Server from one repository, with each component
documented on its own and installable independently.
The v1.0.0 tag sits on the first of three commits carrying this version message. Its
tree holds twelve of the components below: security-monitoring/ and usb-defense/,
and the unified install.sh, are reachable from v1.0.1 and v1.1.0 respectively.
Until then each component installs from its own directory.
Added
Core Components
- fail2ban/: Intrusion prevention. Six jails across
jail.local.templateand
fail2ban/drop-ins/, with GeoIP filtering and Telegram alerts. - ssh-hardening/: SSH hardening with secure sshd_config templates
- nftables/: Modern firewall with modular rule sets
- ufw/: Simplified firewall alternative
- aide/: File integrity monitoring with systemd integration
- lynis/: Security auditing with automation scripts
- rkhunter/: Rootkit detection and prevention
- auditd/: Kernel-level audit logging
- apparmor/: Mandatory access control profiles
Advanced Security
- kernel-hardening/: sysctl security configurations
- boot-security/: GRUB password protection
- usb-defense/: USB device access control
- vaultwarden/: Credential management integration
- security-monitoring/: Prometheus exporters and Grafana dashboards
Documentation
- Each component can be set up without reading the others. Component READMEs plus a
repository README with a Quick Start guide. - An auditor can map the configuration to the benchmark it claims. CIS Benchmark
alignment documentation. - A failed hardening step has a documented first move. Troubleshooting guide and
best practices documentation. - The components report to Prometheus and Grafana. Integration guide with exporters
and dashboards.
Security
- The defaults are the hardened ones. Configurations are aligned with the CIS Ubuntu
Benchmark, and each component ships secure defaults rather than permissive ones that
have to be tightened afterwards. - A single bypassed control does not open the server. Defense-in-depth across the
nine layers listed in this tag's README, from boot through kernel, network, and
detection to audit.