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
41 changes: 36 additions & 5 deletions developer-docs/choria/choria-transport-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<username>.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
`<username>.mcollective` by default. Generate a matching certificate on
the primary server:

```bash
sudo puppetserver ca generate --certname <username>.mcollective
Expand All @@ -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 `<username>.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
Expand Down Expand Up @@ -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: "/.*/"
Expand All @@ -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$/, /.*/"

Expand Down
49 changes: 46 additions & 3 deletions documentation/choria-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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
`<username>.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
`<username>.mcollective` naming convention.

When using OpenBolt through `smart_proxy_openbolt`, the proxy sets this
automatically from its SSL certificate.

## Operations

### run_command
Expand Down Expand Up @@ -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 `<username>.mcollective`, set the
`mcollective-certname` option to the certificate's CN. See
[Non-root certname](#non-root-certname) above.
11 changes: 10 additions & 1 deletion lib/bolt/bolt_option_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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 '<username>.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
Expand Down
1 change: 1 addition & 0 deletions lib/bolt/config/transport/choria.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Choria < Base
config-file
host
interpreters
mcollective-certname
nats-connection-timeout
nats-servers
puppet-environment
Expand Down
9 changes: 9 additions & 0 deletions lib/bolt/config/transport/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 " \
"'<username>.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 " \
Expand Down
5 changes: 5 additions & 0 deletions lib/bolt/transport/choria/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(',')
Expand Down
6 changes: 3 additions & 3 deletions pwsh_module/command.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -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
}
}

Expand All @@ -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
}
}

Expand Down
21 changes: 21 additions & 0 deletions schemas/bolt-defaults.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,16 @@
}
]
},
"mcollective-certname": {
"oneOf": [
{
"$ref": "#/transport_definitions/mcollective-certname"
},
{
"$ref": "#/definitions/_plugin"
}
]
},
"nats-connection-timeout": {
"oneOf": [
{
Expand Down Expand Up @@ -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 '<username>.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": [
Expand Down
11 changes: 11 additions & 0 deletions schemas/bolt-inventory.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<username>.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": [
Expand Down
2 changes: 1 addition & 1 deletion spec/unit/config/transport/choria_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions spec/unit/transport/choria_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
Loading