# debian-cis turns the CIS benchmark into 232 individually configurable shell scripts

> The ovh/debian-cis project is a modular set of shell scripts that implement the CIS Debian hardening recommendations, one file per check, each with a per-check configuration file that selects disabled, audit or enabled. OVHcloud uses it to harden PCI-DSS infrastructure, and the checks number 232 with 166 run by default.

**ovh/debian-cis** — PCI-DSS compliant Debian 12/13 hardening

- Repository: https://github.com/ovh/debian-cis
- Stars: 1,080 · Forks: 179
- Language: Shell
- License: NOASSERTION
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/ovh-debian-cis

## One file per check, one config file per check

The project describes itself as modular Debian 11/12/13 security hardening scripts based on the CIS recommendations, and the modularity is literal. The hardening scripts are in bin/hardening, each script has a corresponding configuration file in etc/conf.d/[script_name].cfg, and each hardening script can be individually enabled from its configuration file.

The example given is the default configuration file for disable_system_accounts.

```
# Configuration for script of same name
status=disabled
# Put here your exceptions concerning admin accounts shells separated by spaces
EXCEPTIONS=""
```

The status parameter takes three values, and they are the whole model of the project. disabled means do nothing, the script will not run. audit means the script will check if any change should be applied. enabled means the script will check if any change should be done and automatically apply what it can.

So every check in the benchmark can be in one of three states, and the state is a file you can read, diff and put under version control. That is the design decision, and it is what makes the tool usable in a compliance context where a reviewer has to be able to see why a particular recommendation is not enforced.

The exception mechanism is per check, as the EXCEPTIONS line shows. A check that disables a filesystem type has a list of exceptions, and a check that restricts admin account shells has a list of exceptions. Every check can have its own carve-outs, which is the difference between a tool that is applied once and a tool that is maintained over years.

Global configuration lives in etc/hardening.cfg, and it controls the log level and the backup directory. Whenever a script is instructed to edit a file, it will create a timestamped backup in that directory, so a change to a system file is always reversible and the previous version is on disk.

## The summary tells you how far you are from conformant

The audit output is worth looking at, because the shape of the summary is the shape of the tool's value.

```console
$ bin/hardening.sh --audit-all
[...]
################### SUMMARY ###################
      Total Available Checks : 232
         Total Runned Checks : 166
         Total Passed Checks : [ 142/166 ]
         Total Failed Checks : [  24/166 ]
   Enabled Checks Percentage : 71.00 %
       Conformity Percentage : 85.00 %
```

Two different percentages, and the distinction is the point. Enabled Checks Percentage, 71.00, is how much of the benchmark you have actually turned on. Conformity Percentage, 85.00, is how many of the checks that are running pass. So the 232 available checks versus 166 run reflects the 71 percent, and the 142 of 166 passing reflects the 85 percent.

That distinction is a genuinely useful piece of reporting. A team that only tracked conformity would see 85 percent and think it was nearly done, when two thirds of the benchmark was never being evaluated. A team that only tracked enabled would see 71 percent and think it had failures it did not have.

The per-check output is the standard CIS benchmark style, with a numbered file name, an INFO line for the working state, a DESCRIPTION line, the check phase and the audit phase, and then a verdict. The example in the README shows 1.1.1.1_disable_freevxfs, the description being Disable mounting of freevxfs filesystems, and the result being CONFIG_VXFS_FS is disabled, Check Passed.

The numbering is the CIS section numbering, which is what makes a failure traceable to a recommendation in the benchmark document. A report line of 6.2.19_check_duplicate_groupname with the verdict No duplicate GIDs maps directly to a numbered item in the CIS guide, so a compliance reviewer can look up exactly what was checked and why it failed.

## The command line has modes for reading, applying and bootstrapping

The usage section gives the main entry point as bin/hardening.sh, with two main operation modes. --audit audits your system with all enabled and audit mode scripts, and --apply audits your system with all enabled and audit mode scripts and applies changes for enabled scripts.

The extra options are what make it usable in a real rollout, and each one addresses a specific operational need.

--audit-all forces running all auditing scripts including disabled ones, and this will not change the system. It is the discovery mode, the one that tells you what the benchmark would say if you enabled everything.

--audit-all-enable-passed runs all scripts in audit mode and, if a script passes, automatically enables it for future runs. The README warns twice about it, once not to use this option if you have already started to customize your configuration, and once in the same spirit for --set-hardening-level. The logic is sound, a passing check is a check your system already satisfies so enabling it is free, but doing that after you have hand-tuned your configuration will overwrite your choices with the default ones.

--set-hardening-level runs all checks lower than or equal to the selected level, with the same warning.

--allow-service modifies the policy to allow a certain kind of services on the machine, such as http or mail, and it can be specified multiple times, with --allow-service-list to see the supported ones. This is the mechanism for saying, this machine runs a web server, so the checks that say disable http must be turned off.

--sudo audits as a normal user but allows sudo escalation to read specific root read-only files, and it requires a sudoers file in /etc/sudoers.d/ with the NOPASWD option because the checks run with sudo -n, which will not prompt for a password. That detail is a real operational requirement, since a prompt in a non-interactive run hangs.

--batch sets the log level to ok and captures all output to print only one line once the check is done, formatted like OK|KO OK|KO|WARN{subcheck results}. That is for CI.

--only runs only the selected checks, and --set-log-level takes info, warning, error, ok or debug.

Two more are worth calling out. --create-config-files-only creates the config files in etc/conf.d, and it must be run as root before running the audit with user secaudit, to have the rights set up correctly on the conf files. And --allow-unsupported-distribution must be specified manually on the command line to allow the run on a non compatible version or distribution, with the warning muted by changing the log level in /etc/hardening.cfg.

## Numbering 99 for your own checks

The Hacking section explains how to add a check, and the numbering convention is the whole trick.

```console
$ cp src/skel bin/hardening/99.99_custom_script.sh
$ chmod +x bin/hardening/99.99_custom_script.sh
$ cp src/skel.cfg etc/conf.d/99.99_custom_script.cfg
```

Every custom check numerotation begins with 99, and the numbering after it depends on the section the check refers to. So 99.99 sorts after every CIS section in the listing, while still pointing at the section it relates to.

There is a second rule for the case where your check replaces one from the CIS specification. If the check replaces one that is in the CIS specifications, you can use the numerotation of the check it replaces in place. The example given is that they check the config of OSSEC, file integrity, in 1.4.x whereas CIS recommends AIDE. So they have taken over a CIS check number for their own implementation of that check, which means an auditor looking at 1.4.x sees the CIS number and gets the OVHcloud check.

There is a third convention, and it is the documentation one. Do not forget to specify in a comment if it is a bonus check, suggested by CIS but not in the CIS numerotation, a legacy check, part from a previous CIS specification but deleted in a more recent one, or an OVHcloud security check. So the numbering tells you where a check sits in the benchmark, and the comment tells you whether it is really from the benchmark or something the vendor added.

The repository listing backs the picture. There is a src/ directory with the skeleton files, an etc/ directory with the configuration, a lib/ directory, a bin/ directory, a hooks/ directory, a versions/ directory for the per-distribution definitions, a utils/ directory, a tests/ directory, a debian/ directory for packaging, and a shellcheck/ and shellfmt/ directory, which are the two linters the project runs on its own shell code, with a workflow badge for each of the shell linter and the functional tests.

## Where debian-cis is the wrong choice

The first wrong case is a team that wants policy as code. The execution model here is running shell scripts on a host, with a per-check configuration file on that host. A team that manages its fleet with Ansible, Puppet, Chef or a configuration management tool already expresses its desired state declaratively, and the natural way to adopt a benchmark is to translate the recommendations into their own configuration management language, where drift is impossible because the tool converges the host. Running a second, parallel system that also wants to change files creates a conflict between the two.

The second wrong case is a non-Debian fleet, or a Debian version the CIS guide does not cover. The --allow-unsupported-distribution flag exists precisely for that case, and it is a flag you have to pass on the command line deliberately, and the README notes the Debian 13 CIS Hardening guide is still in development. The project is honest that it uses the scripts in production on Debian 13 at OVHcloud anyway, which is a reasonable engineering position, but it is a position.

The third wrong case is a team that wants an agent. There is no daemon here, no inventory, no central reporting. The output is a text report on the machine, and the summary percentages are computed locally. A compliance officer who needs a dashboard across a thousand hosts has to build that themselves.

The fourth wrong case is a system that has legitimate, unusual requirements. The per-check EXCEPTIONS mechanism handles a small number of them. A machine whose entire purpose is to run a mail server, or a container host with unusual mount requirements, will have a large fraction of the benchmark inapplicable, and the report will show it as a low conformity percentage even though the system is correctly configured for its purpose.

## How it differs from running the CIS benchmark by hand

The comparison worth making is with what an administrator does without a tool, which is read the CIS Debian guide, work through the numbered recommendations, and fix them one at a time by editing the relevant configuration files.

That is how a lot of hardening actually happens, and it has two characteristics. It produces a defensible result, because you can point at the exact change you made and why. And it does not scale, because there are hundreds of recommendations and a fleet of hosts, and the second host is the same work again.

The difference in approach in this project is that the recommendations are code. Each check is a script with a description, an audit path and an apply path, so the same artefact answers the question of whether a host conforms and of how to make it conform. The audit mode and the enabled mode in the configuration file are that split made explicit.

The second difference is the report. A hand-applied hardening leaves you with a set of change records and, if you are lucky, a checklist. This produces a summary with two percentages, one for coverage and one for conformity, which is the number a compliance conversation is actually about.

The third difference is reversibility. Because every file edit creates a timestamped backup in the configured backup directory, and because the enabled mode only applies what it can, the blast radius of a run is bounded and traceable. A script that does not know how to fix a condition leaves it alone rather than guessing.

The fourth difference is the exception handling. A hand-applied hardening records each exception in the admin's head, or in a wiki, or nowhere. Here an exception is a line in the configuration file next to the check it belongs to, which is both reviewable and diffable.

## Conclusion

debian-cis fits a Debian shop with a PCI-DSS or CIS compliance obligation that wants the checks as reviewable shell scripts rather than an agent, and it fits a team that wants to enable checks one at a time as it builds its configuration. It does not fit a team that wants a policy-as-code pipeline, since the execution model is running scripts on a host. Before running it, start with --audit-all in read-only mode, read the per-check configuration in etc/conf.d before switching anything to enabled, and note that the licence is a custom one GitHub cannot classify, that the newest tagged release is v4.1-6 from 2025-12-17, and that the README states the Debian 13 CIS guide is still in development even though the scripts are used in production on Debian 13 at OVHcloud.

## FAQ

### How do I install and run the ovh debian-cis hardening scripts?

Clone the repository, copy debian/default to /etc/default/cis-hardening, then use sed to point CIS_LIB_DIR, CIS_CHECKS_DIR, CIS_CONF_DIR, CIS_TMP_DIR and CIS_VERSIONS_DIR at the checkout. Run bin/hardening.sh with --audit-all to read the system without changing it, and use --apply to apply the checks that are in enabled mode.

### What do the disabled, audit and enabled statuses mean?

Each hardening script has a matching configuration file in etc/conf.d with a status parameter. disabled means the script will not run, audit means the script will check if any change should be applied, and enabled means it will check and automatically apply what it can.

### How do I add my own hardening check?

Copy src/skel to bin/hardening/99.99_custom_script.sh, make it executable, and copy src/skel.cfg to etc/conf.d/99.99_custom_script.cfg. Every custom check number starts with 99, and if your check replaces one from the CIS specification you can use that check's number instead.

### What do the two percentages in the summary mean?

Enabled Checks Percentage is how much of the benchmark is switched on, 71.00 percent in the README example with 166 of 232 checks run. Conformity Percentage is how many of the checks that actually ran passed, 85.00 percent with 142 of 166 passing.

### Is the ovh debian-cis project maintained and what licence does it use?

The last push to master was on 2026-10-15, with the newest tagged release v4.1-6 from 2025-12-17 and a pre-release from 2026-10-13. The project listing marks the licence as a custom one GitHub cannot classify, with a LICENSE file in the repository, and OVHcloud states it uses the scripts in production to harden its PCI-DSS infrastructure.

## Sources

- [Issues](https://github.com/ovh/debian-cis/issues)
- [ovh/debian-cis on GitHub](https://github.com/ovh/debian-cis)
- [README](https://github.com/ovh/debian-cis/blob/master/README.md)
- [Releases](https://github.com/ovh/debian-cis/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ovh-debian-cis
