In InSpec, how do you override an upstream benchmark profile's sshd rule without forking it?
answer
- inherit, do not copy
- depends in inspec.yml
- include_controls plus skip_control
- inputs instead of hardcoded thresholds
- waiver carries justification and expiry
basics
~20 sWrite a wrapper profile: declare the upstream profile under depends in inspec.yml, pull its controls in with include_controls, skip_control the rule you are replacing, and add your own stricter control or feed the upstream one a different input value.
solid answer
~50 sYou inherit rather than copy. Your profile's `inspec.yml` lists the benchmark under `depends`, and a file in your `controls/` directory calls `include_controls 'cis-linux'` - which runs all of its controls alongside yours - or `require_controls` if you are adopting it in stages. Inside that block, `skip_control 'id'` drops the upstream rule, and you write your own control asserting the stricter thing. Where the upstream control already exposes an `input`, you do not skip anything: you declare a new value in your `inspec.yml` or pass `--input-file prod.yml`, so the same profile serves the bastion and the fleet with different thresholds. Exemptions belong in a waiver file with a `justification` and an `expiration_date` rather than a bare skip, so the report says who accepted the deviation and until when. Forking loses upstream updates and hides which controls you changed.
code
ruby · 16 lines# controls/overrides.rb in the wrapper profile.
# inspec.yml declares: depends: [{ name: cis-linux, path: ../cis-linux }]
# inputs: [{ name: max_auth_tries, type: numeric, value: 3 }]
include_controls 'cis-linux' do
skip_control 'cis-sshd-permitrootlogin' # replaced by the stricter rule below
end
control 'acme-sshd-01' do
impact 0.7
title 'sshd refuses root login and limits authentication attempts'
describe sshd_config do
its('PermitRootLogin') { should cmp 'no' }
its('MaxAuthTries') { should cmp input('max_auth_tries') }
end
endgo deeper
Know that profiles can depend on other profiles and that you are expected to reuse a published benchmark rather than paste it into your repo. Recognise depends, include_controls and input in a profile you are shown.
Be ready to write the wrapper: the depends entry, include_controls with a skip_control, and your replacement control reading a value through input(). Explain why the threshold is an input rather than a literal.
Demonstrate the exemption discipline - waiver with justification and expiry over a silent skip - and describe how you pin and bump the upstream dependency so a new benchmark revision shows up as a readable diff in results.
Own the standard: one wrapper per estate that reads as the complete list of deviations from the published benchmark, with an expiry on every exemption, so the question 'where do we differ from the benchmark and why' has a single answer.
### The problem Upstream hardening profiles are published as whole benchmarks. Yours is *almost* right: one threshold is too loose for your estate, one control cannot apply to the bastion hosts, and everything else you want verbatim — including next quarter's upstream updates. The wrong answer, and the common one, is to copy the profile into your repository and edit it. From that moment you own a fork: upstream fixes have to be merged by hand, and nobody can tell from the outside which controls you changed and why. ### Wrapper profiles InSpec's answer is **profile inheritance**. You create your own profile whose `inspec.yml` declares the upstream one as a dependency: ```yaml name: acme-linux-baseline depends: - name: cis-linux path: ../cis-linux # or url:, git:, supermarket: inputs: - name: max_auth_tries type: numeric value: 3 ``` Then, in your own `controls/` directory, you pull the upstream controls in: - `include_controls 'cis-linux'` runs **all** of the dependency's controls alongside yours. - `require_controls 'cis-linux' do control 'id-1' ... end` runs **only** the ones you name — useful when you are adopting a large benchmark in stages. - Inside either block, `skip_control 'id'` removes a single upstream control from the run. It reports as skipped, so the exclusion is visible in the result rather than silently absent. Your own controls live beside the included ones, so the natural way to tighten a rule is: skip the upstream control and write your own with a stricter assertion and its own id. The report then shows both facts — the upstream rule was excluded, and a replacement ran. ### Inputs: one profile, many thresholds Hardcoding `should cmp 3` in a control body means a second environment needs a second profile. **Inputs** are the parameterisation mechanism: declared in `inspec.yml` with a name, type and default, read in a control as `input('max_auth_tries')`, and overridden at run time with `--input name=value` or, for anything non-trivial, `--input-file prod.yml`. Where the upstream benchmark already exposes an input for the value you disagree with, you do not need a skip or an override at all — you feed a different value and keep the upstream control intact, which is the cheapest possible divergence. ### Exemptions belong in a waiver, not a skip `skip_control` says *this never runs here*. It carries no reason and no end date. For "the bastion legitimately allows a login method the benchmark forbids", the better mechanism is a **waiver file** — YAML keyed by control id, carrying a `justification`, an `expiration_date`, and `run: true|false`. With `run: false` the control is not executed and is reported as waived; with `run: true` it executes and its result is reported but marked waived. Either way the report answers the auditor's actual question — *who decided this was acceptable, on what grounds, and until when* — and an expired waiver stops applying by itself, so exemptions do not quietly become permanent. ### The same shape elsewhere `oscap` solves this with an **XCCDF tailoring file**: a separate document that selects and deselects rules from a benchmark and refines the values its rules compare against, passed at evaluation time with `--tailoring-file`. The benchmark content stays untouched and byte-identical to what the publisher shipped; your deviations are a small, reviewable artifact next to it. Same principle, different serialisation: keep the upstream content pristine and express your divergence as data. ### What good looks like A wrapper profile is small and reads like a changelog against the benchmark. It is version-controlled, its dependency is pinned to a specific upstream version, and every `skip_control` has a comment or a corresponding waiver explaining itself. When upstream publishes a new revision you bump the dependency, run it, and read the diff in results — which is only possible because you never forked. The reviewable question an interviewer is really asking is: *can you show me, in one file, everywhere your estate deviates from the published benchmark?* A wrapper answers that in seconds; a fork cannot answer it at all.
- Where do you record that the bastion is legitimately exempt from one control?In a waiver file, not a `skip_control`. A waiver entry is keyed by control id and carries a `justification`, an `expiration_date` and `run: true|false`, so the result is reported as waived with a reason attached and stops being waived once the date passes. A bare skip records no reason and never expires, which is how temporary exemptions become permanent ones.
- What do you lose by copying the benchmark profile into your repository and editing it?Upstream updates and reviewability. Every new revision of the benchmark becomes a manual merge, and nobody can see at a glance which controls you changed, because your deviations are mixed into thousands of lines you did not write. A wrapper is small enough to read as a changelog against the published benchmark, which is exactly the artifact an auditor asks for.
- How do you run one profile against environments that need different thresholds?Parameterise with inputs. Declare the value in `inspec.yml` with a name, type and default, read it in the control as `input('name')`, and override it per environment at run time with `--input name=value` or an `--input-file`. One profile version, one set of controls, environment-specific values supplied as data rather than as a second copy of the profile.
saying these in an interview costs you the question
- Copies the upstream profile into the repo and edits it in place
- Deletes failing controls instead of waiving them with a reason
- Hardcodes environment-specific thresholds in the control body
- Thinks lowering impact turns a failing control into a pass
- Leaves exemptions in a run-time flag that the report never shows