Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ jobs:
ruby_version: '2.7'
env:
PUPPET_GEM_VERSION: ${{matrix.puppet.puppet_version}}
BUNDLE_WITHOUT: 'acceptance:system_tests'
steps:
- uses: actions/checkout@v2
- name: Install libcurl dev headers (needed to build the patron gem pulled in by faraday-patron)
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{matrix.puppet.ruby_version}}
Expand All @@ -41,8 +44,11 @@ jobs:
ruby_version: '2.7'
env:
PUPPET_GEM_VERSION: ${{matrix.puppet.puppet_version}}
BUNDLE_WITHOUT: 'acceptance:system_tests'
steps:
- uses: actions/checkout@v2
- name: Install libcurl dev headers (needed to build the patron gem pulled in by faraday-patron)
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{matrix.puppet.ruby_version}}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@
.envrc
/inventory.yaml
/spec/fixtures/litmus_inventory.yaml
/CLAUDE.md
42 changes: 42 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# AGENTS.md

This file provides guidance to AI coding assistants when working with code in this repository.

## Overview

Puppet module (`ayohrling-local_security_policy`) that manages Windows Local Security Policy (password policy, account lockout, audit policy, user rights assignment, security options) via the Windows `secedit` tool. Built with PDK; supports Puppet 6/7 on Windows only, but unit tests run on Linux.

## Commands

```sh
bundle install # install dependencies (PDK-managed Gemfile)
bundle exec rake parallel_spec # run all unit tests (what CI runs)
bundle exec rspec spec/unit/puppet/provider/local_security_policy/policy_spec.rb # single spec file
bundle exec rspec spec/unit/puppet/type/local_security_policy/local_security_policy_spec.rb -e 'some example' # single example
bundle exec rake syntax lint metadata_lint check:symlinks check:git_ignore check:dot_underscore check:test_file rubocop # CI syntax/lint job
```

Set `PUPPET_GEM_VERSION='~> 7'` (or `~> 6`) to pin the Puppet version, matching the CI matrix in `.github/workflows/pr.yml`.

Acceptance tests use beaker + Vagrant against Windows boxes (`spec/acceptance/`, nodesets in `spec/acceptance/nodesets/`), e.g. `BEAKER_set=win2022 bundle exec rspec spec/acceptance/suites/`. They require Vagrant with Windows VMs and are not run in CI.

## Architecture

The module is a single custom type/provider pair plus a supporting data-mapping class:

- `lib/puppet_x/lsp/security_policy.rb` — the heart of the module. `SecurityPolicy.lsp_mapping` is a large hash mapping every supported policy's GUI display name (e.g. `'Maximum password age'`) to its secedit key (`name:`), its INF section (`policy_type:` — one of `System Access`, `Event Audit`, `Privilege Rights`, `Registry Values`), and optionally `reg_type:` (registry value type as string) and `data_type:` (`:principal` for user/SID lists, `:quoted_string`). It also holds value-conversion logic: user names ↔ SIDs (sorted, `*`-prefixed), audit words ↔ numeric IDs, and registry values to `reg_type,value` form. **Adding support for a new policy setting usually means only adding an entry to `lsp_mapping`** (keep entries grouped by category and alphabetized within their section, matching the GUI name exactly).

- `lib/puppet/type/local_security_policy.rb` — the type. Resource title must exactly match a GUI name in `lsp_mapping` (validated via `SecurityPolicy.valid_lsp?`). `policy_type` and `policy_setting` are informational: they're always derived/overridden from the mapping in `defaultto`/`munge`, never trusted from the manifest. `policy_value` is validated per policy type and munged through `SecurityPolicy.convert_policy_value` so comparisons against system state are idempotent (e.g. SIDs are sorted before joining — see commit "Fix SID order idempotency").

- `lib/puppet/provider/local_security_policy/policy.rb` — the provider. `self.instances` runs `secedit /export`, parses the INF (encoded IBM437/UTF-16LE, converted to UTF-8) with the vendored inifile, and maps each key back to a display name via `find_mapping_from_policy_name` (unmapped system keys are silently skipped). Writes happen one policy at a time in `flush` by generating a small INF and calling `secedit /configure`. `destroy` is intentionally a no-op — LSP settings can't be safely removed.

- `lib/puppet_x/twp/inifile.rb` — vendored INI parser/writer used for secedit INF files. Don't rewrite it; it handles secedit's quirks.

- `manifests/init.pp` — thin Hiera wrapper: `local_security_policy::policies` hash creates `local_security_policy` resources.

Both the type and provider carry a `LoadError` rescue block for loading `puppet_x` code across pluginsync layouts — preserve it when editing requires.

## Testing notes

- Unit specs stub all Windows interaction: `secedit` calls, `Puppet::Util::Windows::SID` lookups, and file I/O are mocked, with sample exported INF data in `spec/fixtures/unit/puppet/provider/local_security_policy/` (`secedit.inf`, `short_secedit.inf`).
- `spec/spec_helper.rb` and most boilerplate are PDK-managed (`pdk-version` in `metadata.json`); avoid hand-editing PDK-templated files.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [1.2.0] - 2026-07-16
- Add support for 'Domain controller' security options (Allow server operators to schedule tasks, Allow vulnerable Netlogon secure channel connections, LDAP server channel binding token requirements, LDAP server signing requirements, Refuse machine account password changes)

## [1.1.1] - 2023-01-03
- Fixed SID order idempotency - #124

Expand Down
25 changes: 25 additions & 0 deletions lib/puppet_x/lsp/security_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,31 @@ def self.lsp_mapping
reg_type: '1',
policy_type: 'Registry Values',
},
'Domain controller: Allow server operators to schedule tasks' => {
name: 'MACHINE\System\CurrentControlSet\Control\Lsa\SubmitControl',
reg_type: '4',
policy_type: 'Registry Values',
},
'Domain controller: Allow vulnerable Netlogon secure channel connections' => {
name: 'MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\VulnerableChannelAllowList',
reg_type: '1',
policy_type: 'Registry Values',
},
'Domain controller: LDAP server channel binding token requirements' => {
name: 'MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LdapEnforceChannelBinding',
reg_type: '4',
policy_type: 'Registry Values',
},
'Domain controller: LDAP server signing requirements' => {
name: 'MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity',
reg_type: '4',
policy_type: 'Registry Values',
},
'Domain controller: Refuse machine account password changes' => {
name: 'MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RefusePasswordChange',
reg_type: '4',
policy_type: 'Registry Values',
},
'Domain member: Digitally encrypt or sign secure channel data (always)' => {
name: 'MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RequireSignOrSeal',
reg_type: '4',
Expand Down
2 changes: 1 addition & 1 deletion metadata.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ayohrling-local_security_policy",
"version": "1.1.1",
"version": "1.2.0",
"author": "Paul S. Cannon, Adam Yohrling, Corey Osman, Ryan Russell-Yates, Jordan Wesolowski, Gerben Welter, Thomas Linkin, Andy Adrian, Steven Pritchard",
"summary": "Windows Local Security Policy management. Forked from cannonps/local_security_policy",
"license": "Apache-2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,42 @@
end
end

describe 'domain controller policies' do
{
'Domain controller: Allow server operators to schedule tasks' =>
['MACHINE\System\CurrentControlSet\Control\Lsa\SubmitControl', '4'],
'Domain controller: Allow vulnerable Netlogon secure channel connections' =>
['MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\VulnerableChannelAllowList', '1'],
'Domain controller: LDAP server channel binding token requirements' =>
['MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LdapEnforceChannelBinding', '4'],
'Domain controller: LDAP server signing requirements' =>
['MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity', '4'],
'Domain controller: Refuse machine account password changes' =>
['MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RefusePasswordChange', '4'],
}.each do |policy_desc, (reg_key, reg_type)|
it "maps #{policy_desc}" do
mapping = SecurityPolicy.find_mapping_from_policy_desc(policy_desc)
expect(mapping[:name]).to eq(reg_key)
expect(mapping[:reg_type]).to eq(reg_type)
expect(mapping[:policy_type]).to eq('Registry Values')
end

it "reverse maps #{reg_key}" do
name, = SecurityPolicy.find_mapping_from_policy_name(reg_key)
expect(name).to eq(policy_desc)
end
end

it 'converts a REG_DWORD registry value' do
expect(subject.convert_registry_value('Domain controller: LDAP server signing requirements', 2)).to eq('4,2')
end

it 'converts a REG_SZ registry value' do
expect(subject.convert_registry_value('Domain controller: Allow vulnerable Netlogon secure channel connections',
'O:BAG:BAD:(A;;RC;;;BA)')).to eq('1,O:BAG:BAD:(A;;RC;;;BA)')
end
end

describe 'privilege right' do
let(:resource) do
Puppet::Type.type(:local_security_policy).new(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@
expect(resource[:policy_value]).to eq('4,0')
end

it 'creates a domain controller registry value and derives its setting from the mapping' do
resource = Puppet::Type.type(:local_security_policy).new(
name: 'Domain controller: LDAP server signing requirements',
ensure: 'present',
policy_value: '2',
)
expect(resource[:policy_value]).to eq('4,2')
expect(resource[:policy_setting]).to eq('MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity')
end

it 'creates a domain controller string registry value' do
resource = Puppet::Type.type(:local_security_policy).new(
name: 'Domain controller: Allow vulnerable Netlogon secure channel connections',
ensure: 'present',
policy_value: 'O:BAG:BAD:(A;;RC;;;BA)',
)
expect(resource[:policy_value]).to eq('1,O:BAG:BAD:(A;;RC;;;BA)')
expect(resource[:policy_setting]).to eq('MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\VulnerableChannelAllowList')
end

it 'creates an event audit value' do
resource = Puppet::Type.type(:local_security_policy).new(
name: 'Audit account logon events',
Expand Down
Loading