Convert and Filter Sudoers Policy with cvtsudoers
Auditors want every rule touching one group, and cvtsudoers turns a sprawling sudoers file into something you can actually query for that. You will convert a sudoers policy to JSON or CSV, filter the result, and reconstruct sudoers text when that is what you need instead. The examples were checked with sudo 1.9.15p5-3ubuntu5.24.04.2, whose cvtsudoers reports grammar version 50. Allow about 15 minutes.
The route
Jump straight to the step you need, or tick off Done means at the end.
You need a shell and a syntactically valid policy file. None of the examples needs elevated privilege if you can already read that file.
Safety boundary
cvtsudoers reads a policy and writes converted output; it does not install a policy, validate a live sudo configuration, or grant access on its own. Keep output in a temporary file until a separate review and deployment process signs off, and never point -o at /etc/sudoers or an included file while experimenting.
1. Confirm the installed tool
Check the version and grammar before trusting an example, including this one. It is an ordinary, non-privileged command:
$ cvtsudoers -V
cvtsudoers version 1.9.15p5
cvtsudoers grammar version 50
The default input format is sudoers, and the default output format is LDIF. Spell out both formats explicitly in any script, so a future package upgrade cannot turn an unlabelled output file into a different format overnight.
Checkpoint
If cvtsudoers -V fails, stop and repair the sudo package through your normal process rather than reaching for an unverified converter.
2. Convert a policy to JSON
Feed it a small policy on standard input first, to learn the shape of the output before you touch anything real:
$ printf '%s\n' \
'User_Alias ADMINS = alice, %wheel' \
'ADMINS ALL=(root) /usr/bin/systemctl status nginx' \
| cvtsudoers -f json -
{
"User_Aliases": {
"ADMINS": [
{ "username": "alice" },
{ "usergroup": "wheel" }
]
},
"User_Specs": [
{
"User_List": [
{ "useralias": "ADMINS" }
],
"Host_List": [
{ "hostname": "ALL" }
],
"Cmnd_Specs": [
{
"runasusers": [
{ "username": "root" }
],
"Commands": [
{ "command": "/usr/bin/systemctl status nginx" }
]
}
]
}
]
}
A file argument works the same way:
$ cvtsudoers -i sudoers -f json /path/to/policy.sudoers > /tmp/policy.json
The input has to parse cleanly first. Includes and comments get represented however the target format allows, so do not assume JSON is a line-for-line transcript of the original file.
3. Inspect a smaller CSV report
CSV suits a spreadsheet or a quick report. Filter by user, host, group or command with -m, a comma-separated list of key=value pairs:
$ cvtsudoers -f csv -m user=alice - /path/to/policy.sudoers
rule,user,host,runusers,rungroups,options,command
rule,ADMINS,ALL,root,,"",/usr/bin/systemctl status nginx
Supported filter keys are user, group, host, and cmnd, with cmd accepted as a shorter spelling. Matching does not consult local password or group databases by default, so a name can match the policy text even when that account does not exist on this machine.
Add -M only when the filter genuinely needs local passwd and group data. With it, users and groups named in the filter must exist locally, and a user's group memberships can change what matches. This is a security-relevant reporting choice: it can alter what shows up in the report without touching the source policy at all.
4. Prune a filtered policy when you need to
A matching rule can still list other users, groups or hosts. Add -p when the output must drop those non-matching members:
$ cvtsudoers -i sudoers -f json -m user=alice -p \
/path/to/policy.sudoers > /tmp/alice-policy.json
Review the result before using it anywhere. Filtering is not an access-control change by itself, and pruning can leave a policy fragment unfit to stand in for the original. Leave the source file alone so recovery is nothing more than deleting the temporary report.
5. Reconstruct sudoers text for review
To get back traditional sudoers syntax, name that output format explicitly:
$ cvtsudoers -i sudoers -f sudoers /path/to/policy.sudoers > /tmp/reconstructed.sudoers
$ sed -n '1,80p' /tmp/reconstructed.sudoers
The file gets parsed and rebuilt from scratch. Comments do not survive, and data from included files is inlined. Aliases are normally kept in sudoers output; add -e only if you specifically want them expanded instead.
Before any deployment, run the reconstructed file through your distribution's sudo validation step and check its permissions and ownership. Replacing a live sudoers file is an elevated, service-impacting security change. The conversion itself needs no sudo; save elevated privileges for the separately approved installation step.
6. Convert or merge several policies
Multiple input files merge into a single policy. When the same logical policy comes from different hosts, prefix each filename with its host name and a colon:
$ cvtsudoers -f json \
web01:/path/to/web.sudoers \
db01:/path/to/db.sudoers \
> /tmp/merged.json
The converter assumes alias names are consistent across the files. It drops duplicate aliases and renames conflicting ones with a numeric suffix such as SERVERS_1, updating references to match. It also warns about conflicting Defaults settings and about anything that cannot be made host-specific. Capture those warnings when the merge actually matters:
$ cvtsudoers -f json -l /tmp/cvtsudoers-warnings.log \
web01:/path/to/web.sudoers db01:/path/to/db.sudoers \
> /tmp/merged.json
$ sed -n '1,120p' /tmp/cvtsudoers-warnings.log
Read both the merged output and the warning log. A clean exit status is not proof that every semantic conflict resolved the way you intended.
Common traps
- Syntax error: fix the source policy first. cvtsudoers can only convert a policy that is already syntactically correct.
- Unexpectedly empty or narrow output: check the
-mfilter, and remember-pstrips non-matching members. - LDIF needs a base DN: converting to LDIF wants
-b ou=SUDOers,dc=example,dc=orgor the configuredSUDOERS_BASEvalue. LDIF also cannot represent every sudoers-specific Defaults or alias construct. - Output looks nothing like the input: comments are gone, includes are inlined for sudoers output, and the target format brings its own shape.
- Options seem to be ignored: command-line options override
/etc/cvtsudoers.confby default, so check that file if an unlabelled invocation behaves oddly.
Done means
- Confirmed the version with cvtsudoers -V before relying on it.
- Converted to an explicitly named format, never left to the default.
- Chose filtering and -M deliberately, with -p used only when pruning was actually intended.
- Reviewed warnings and merged output separately, not just the exit status.
- Left the live sudoers file untouched throughout the conversion and review.