diff --git a/developer-docs/choria/choria-transport-testing.md b/developer-docs/choria/choria-transport-testing.md index 07b88bfcd..da236a60f 100644 --- a/developer-docs/choria/choria-transport-testing.md +++ b/developer-docs/choria/choria-transport-testing.md @@ -79,11 +79,16 @@ MCollective looks for client config files in this order (first readable wins): 3. `/etc/choria/client.conf` 4. `/etc/puppetlabs/mcollective/client.cfg` -### Generate a client certificate +### Client certificate -The user running OpenBolt needs a certificate signed by the Puppet CA. For non-root -users, MCollective resolves the certname as `.mcollective` by default. -Generate a matching certificate on the primary server: +The user running OpenBolt needs a certificate signed by the Puppet CA. +There are two approaches: + +**Option A: Generate a `.mcollective` certificate (traditional)** + +For non-root users, MCollective resolves the certname as +`.mcollective` by default. Generate a matching certificate on +the primary server: ```bash sudo puppetserver ca generate --certname .mcollective @@ -103,6 +108,29 @@ sudo chown -R $(whoami) ~/.puppetlabs chmod 600 ~/.puppetlabs/etc/puppet/ssl/private_keys/*.pem ``` +**Option B: Reuse the host's Puppet certificate** + +If the non-root user can read the host's Puppet SSL files (e.g. via +group membership in the `puppet` group), you can skip certificate +generation and use the host's own cert with `--choria-mcollective-certname` +to override the automatic `.mcollective` certname: + +```bash +bolt task run facts --targets node1.example.com \ + --transport choria \ + --choria-ssl-cert /etc/puppetlabs/puppet/ssl/certs/$(hostname -f).pem \ + --choria-ssl-key /etc/puppetlabs/puppet/ssl/private_keys/$(hostname -f).pem \ + --choria-ssl-ca /etc/puppetlabs/puppet/ssl/certs/ca.pem \ + --choria-mcollective-certname $(hostname -f) +``` + +This requires a permissive `certname_whitelist` on the Choria servers +(e.g. `/.*/`) since the host's FQDN does not match the default +`\.mcollective$` pattern. In production, use a more restrictive pattern +such as `/.*\.example\.com$/` to limit which certnames are accepted. See +[choria-transport.md](../../documentation/choria-transport.md#non-root-certname) +for details. + ### Set up `~/.choriarc` Create `~/.choriarc` with the NATS broker address and cert paths. Replace @@ -270,7 +298,8 @@ choria::server_config: plugin.choria.middleware_hosts: "primary.example.com:4222" plugin.choria.use_srv: false -# Allow all callers for testing. Restrict in production. +# Allow all callers for testing. In production, restrict callers to specific +# certnames or a domain pattern like /.*\.example\.com$/. mcollective::site_policies: - action: "allow" callers: "/.*/" @@ -279,6 +308,8 @@ mcollective::site_policies: classes: "*" mcollective::client: true +# Allow all certnames for testing. In production, replace /.*/ with a pattern +# matching your environment, such as /.*\.example\.com$/. mcollective_choria::config: security.certname_whitelist: "/\\.mcollective$/, /.*/" diff --git a/documentation/choria-transport.md b/documentation/choria-transport.md index 4882f070b..fc490f8e4 100644 --- a/documentation/choria-transport.md +++ b/documentation/choria-transport.md @@ -95,6 +95,7 @@ targets: | `config-file` | `--choria-config-file` | String | (auto-detected) | Path to a Choria/MCollective client config file. | | `host` | | String | (from URI) | Target's Choria identity (FQDN). Overrides the hostname from the URI. | | `interpreters` | | Hash | (none) | File extension to interpreter mapping (e.g., `{".rb": "/usr/bin/ruby"}`). | +| `mcollective-certname` | `--choria-mcollective-certname` | String | (auto) | Override the MCollective certname for Choria client identity. See [Non-root certname](#non-root-certname) below. | | `nats-connection-timeout` | `--nats-connection-timeout` | Integer | `30` | Seconds to wait for the TCP connection to the NATS broker. | | `nats-servers` | `--nats-servers` | String or Array | (from config file) | NATS broker addresses in `nats://host:port` format (comma-separated for multiple). Multiple servers provide failover if a broker is unavailable. Overrides the config file. | | `puppet-environment` | `--choria-puppet-environment` | String | `production` | Puppet environment for bolt_tasks file URIs. | @@ -130,6 +131,47 @@ the Choria config file. you must provide all three. Partial SSL configurations are rejected during validation. +### Non-root certname + +The `choria-mcorpc-support` library derives the MCollective certname as +`.mcollective` for non-root users. This certname is embedded +in signed messages and validated against the SSL certificate's CN during +`check_ssl_setup`. When running as a non-root user (e.g. `foreman-proxy`) +with the host's own Puppet certificate, the automatic certname +(`foreman-proxy.mcollective`) does not match the certificate's CN +(the host's FQDN), causing authentication failures. + +The `mcollective-certname` option overrides this automatic derivation. +Set it to the CN of the certificate you are authenticating with: + +```bash +bolt task run facts --targets node1.example.com \ + --transport choria \ + --choria-ssl-cert /etc/puppetlabs/puppet/ssl/certs/primary.example.com.pem \ + --choria-ssl-key /etc/puppetlabs/puppet/ssl/private_keys/primary.example.com.pem \ + --choria-ssl-ca /etc/puppetlabs/puppet/ssl/certs/ca.pem \ + --choria-mcollective-certname primary.example.com +``` + +Or in the inventory file: + +```yaml +config: + transport: choria + choria: + mcollective-certname: primary.example.com + ssl-cert: /etc/puppetlabs/puppet/ssl/certs/primary.example.com.pem + ssl-key: /etc/puppetlabs/puppet/ssl/private_keys/primary.example.com.pem + ssl-ca: /etc/puppetlabs/puppet/ssl/certs/ca.pem +``` + +This is not needed when running as root (the library uses the configured +identity directly) or when using a certificate that matches the +`.mcollective` naming convention. + +When using OpenBolt through `smart_proxy_openbolt`, the proxy sets this +automatically from its SSL certificate. + ## Operations ### run_command @@ -386,6 +428,7 @@ local modulepath matters. long-running infrastructure. 11. **MCollective client library refuses to run as root.** Use a non-root - user with a Puppet CA-signed certificate. See the - [testing guide](choria-transport-testing.md#running-bolt-as-a-non-root-user) - for setup instructions. + user with a Puppet CA-signed certificate. When using a certificate + whose CN does not match `.mcollective`, set the + `mcollective-certname` option to the certificate's CN. See + [Non-root certname](#non-root-certname) above. diff --git a/lib/bolt/bolt_option_parser.rb b/lib/bolt/bolt_option_parser.rb index c18055270..f7c0fee84 100644 --- a/lib/bolt/bolt_option_parser.rb +++ b/lib/bolt/bolt_option_parser.rb @@ -13,7 +13,8 @@ class BoltOptionParser < OptionParser run_context: %w[concurrency inventoryfile save-rerun cleanup puppetdb], global_config_setters: PROJECT_PATHS + %w[modulepath], transports: %w[transport connect-timeout tty native-ssh ssh-command copy-command], - choria: %w[choria-config-file choria-ssl-ca choria-ssl-cert choria-ssl-key + choria: %w[choria-config-file choria-mcollective-certname + choria-ssl-ca choria-ssl-cert choria-ssl-key choria-collective choria-puppet-environment choria-rpc-timeout choria-task-timeout choria-command-timeout nats-servers nats-connection-timeout], @@ -1108,6 +1109,14 @@ def initialize(options) 'Path to a Choria/MCollective client configuration file.') do |path| @options[:'config-file'] = path end + define('--choria-mcollective-certname NAME', + 'Override the MCollective certname for Choria client identity.', + 'The choria-mcorpc-support library identifies non-root clients', + "as '.mcollective', which fails when authenticating", + "with a certificate that has a different CN (e.g. the host's", + 'Puppet cert). Set this to the CN of the certificate being used.') do |name| + @options[:'mcollective-certname'] = name + end define('--choria-ssl-ca PATH', 'CA certificate path for Choria TLS authentication.') do |path| @options[:'ssl-ca'] = path diff --git a/lib/bolt/config/transport/choria.rb b/lib/bolt/config/transport/choria.rb index 8c90011fd..4e628abf0 100644 --- a/lib/bolt/config/transport/choria.rb +++ b/lib/bolt/config/transport/choria.rb @@ -14,6 +14,7 @@ class Choria < Base config-file host interpreters + mcollective-certname nats-connection-timeout nats-servers puppet-environment diff --git a/lib/bolt/config/transport/options.rb b/lib/bolt/config/transport/options.rb index 949365863..87f2bdaaa 100644 --- a/lib/bolt/config/transport/options.rb +++ b/lib/bolt/config/transport/options.rb @@ -275,6 +275,15 @@ module Options _plugin: true, _example: %w[defaults hmac-md5] }, + "mcollective-certname" => { + type: String, + description: "Override the MCollective certname used for Choria client identity. " \ + "The choria-mcorpc-support library identifies non-root clients as " \ + "'.mcollective'. Set this when authenticating with a certificate " \ + "whose CN differs from that default (e.g. the host's Puppet cert).", + _plugin: true, + _example: "primary.example.com" + }, "nats-servers" => { type: [String, Array], description: "One or more NATS server addresses for the Choria transport. Overrides the middleware " \ diff --git a/lib/bolt/transport/choria/client.rb b/lib/bolt/transport/choria/client.rb index c5b72725d..bd19113f2 100644 --- a/lib/bolt/transport/choria/client.rb +++ b/lib/bolt/transport/choria/client.rb @@ -68,6 +68,11 @@ def configure_client(target) logger.debug { "Loaded Choria client config from #{config_file}" } end + if opts['mcollective-certname'] + ENV['MCOLLECTIVE_CERTNAME'] = opts['mcollective-certname'] + logger.debug { "MCOLLECTIVE_CERTNAME set to #{opts['mcollective-certname']}" } + end + if opts['nats-servers'] servers = [opts['nats-servers']].flatten config.pluginconf['choria.middleware_hosts'] = servers.join(',') diff --git a/pwsh_module/command.tests.ps1 b/pwsh_module/command.tests.ps1 index d3ffa46ec..cdbe93b13 100644 --- a/pwsh_module/command.tests.ps1 +++ b/pwsh_module/command.tests.ps1 @@ -55,7 +55,7 @@ Describe "test bolt command syntax" { It "has correct number of parameters" { ($command.Parameters.Values | Where-Object { $_.name -notin $common - } | measure-object).Count | Should -Be 49 + } | measure-object).Count | Should -Be 50 } } @@ -73,7 +73,7 @@ Describe "test bolt command syntax" { It "has correct number of parameters" { ($command.Parameters.Values | Where-Object { $_.name -notin $common - } | measure-object).Count | Should -Be 46 + } | measure-object).Count | Should -Be 47 } } @@ -95,7 +95,7 @@ Describe "test bolt command syntax" { It "has correct number of parameters" { ($command.Parameters.Values | Where-Object { $_.name -notin $common - } | measure-object).Count | Should -Be 47 + } | measure-object).Count | Should -Be 48 } } diff --git a/schemas/bolt-defaults.schema.json b/schemas/bolt-defaults.schema.json index 9cc2b312d..43c49a131 100644 --- a/schemas/bolt-defaults.schema.json +++ b/schemas/bolt-defaults.schema.json @@ -578,6 +578,16 @@ } ] }, + "mcollective-certname": { + "oneOf": [ + { + "$ref": "#/transport_definitions/mcollective-certname" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "nats-connection-timeout": { "oneOf": [ { @@ -2145,6 +2155,17 @@ } ] }, + "mcollective-certname": { + "description": "Override the MCollective certname used for Choria client identity. The choria-mcorpc-support library identifies non-root clients as '.mcollective'. Set this when authenticating with a certificate whose CN differs from that default (e.g. the host's Puppet cert).", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "nats-servers": { "description": "One or more NATS server addresses for the Choria transport. Overrides the middleware hosts from the Choria client configuration file. Can be a single string or an array.", "oneOf": [ diff --git a/schemas/bolt-inventory.schema.json b/schemas/bolt-inventory.schema.json index bce38b0f9..29c5f0403 100644 --- a/schemas/bolt-inventory.schema.json +++ b/schemas/bolt-inventory.schema.json @@ -139,6 +139,17 @@ } ] }, + "mcollective-certname": { + "description": "Override the MCollective certname used for Choria client identity. The choria-mcorpc-support library identifies non-root clients as '.mcollective'. Set this when authenticating with a certificate whose CN differs from that default (e.g. the host's Puppet cert).", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "nats-connection-timeout": { "description": "How long to wait in seconds for the initial TCP connection to the NATS broker. If the connection cannot be made within this time, the operation fails.", "oneOf": [ diff --git a/spec/unit/config/transport/choria_spec.rb b/spec/unit/config/transport/choria_spec.rb index 24ef024d0..70c22a7f1 100644 --- a/spec/unit/config/transport/choria_spec.rb +++ b/spec/unit/config/transport/choria_spec.rb @@ -22,7 +22,7 @@ context 'validating' do include_examples 'interpreters' - %w[task-agent config-file collective host puppet-environment ssl-ca ssl-cert ssl-key tmpdir].each do |opt| + %w[task-agent config-file collective host mcollective-certname puppet-environment ssl-ca ssl-cert ssl-key tmpdir].each do |opt| it "#{opt} rejects non-string value" do data[opt] = 100 expect { transport.new(data) }.to raise_error(Bolt::ValidationError) diff --git a/spec/unit/transport/choria_spec.rb b/spec/unit/transport/choria_spec.rb index 7c330f7bd..95d0f55cf 100644 --- a/spec/unit/transport/choria_spec.rb +++ b/spec/unit/transport/choria_spec.rb @@ -9,6 +9,27 @@ include_context 'choria task' include BoltSpec::Sensitive + describe '#configure_client mcollective-certname' do + before(:each) do + ENV.delete('MCOLLECTIVE_CERTNAME') + end + + after(:each) do + ENV.delete('MCOLLECTIVE_CERTNAME') + end + + it 'sets MCOLLECTIVE_CERTNAME when mcollective-certname option is provided' do + inventory.set_config(target, %w[choria mcollective-certname], 'primary.example.com') + transport.configure_client(target) + expect(ENV['MCOLLECTIVE_CERTNAME']).to eq('primary.example.com') + end + + it 'does not set MCOLLECTIVE_CERTNAME when mcollective-certname is not provided' do + transport.configure_client(target) + expect(ENV.key?('MCOLLECTIVE_CERTNAME')).to be false + end + end + describe '#provided_features' do it 'includes shell and powershell' do expect(transport.provided_features).to eq(%w[shell powershell])