enh(passwordmgr-centreonvault): adapt vault module for plugins to the new specs (#5302)
Refs: CTOR-1151
This commit is contained in:
parent
4753f295a5
commit
651095a4ea
|
@ -30,6 +30,7 @@ provides:
|
|||
overrides:
|
||||
rpm:
|
||||
depends: [
|
||||
perl(Crypt::OpenSSL::AES),
|
||||
perl(Digest::MD5),
|
||||
perl(Pod::Find),
|
||||
perl-Net-Curl,
|
||||
|
@ -62,6 +63,7 @@ overrides:
|
|||
[@RPM_PROVIDES@]
|
||||
deb:
|
||||
depends: [
|
||||
libcrypt-openssl-aes-perl,
|
||||
libpod-parser-perl,
|
||||
libnet-curl-perl,
|
||||
liburi-encode-perl,
|
||||
|
|
|
@ -335,7 +335,7 @@ Centreon Vault password manager
|
|||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Allows to retrieve secrets (usually username and password) from a Hashicorp vault compatible api given a config file as constructor.
|
||||
Allows to retrieve secrets (usually username and password) from a Hashicorp vault compatible API.
|
||||
|
||||
use centreon::vmware::logger;
|
||||
use centreon::script::centreonvault;
|
||||
|
@ -364,8 +364,8 @@ The expected file format for Centreon Vault is:
|
|||
|
||||
{
|
||||
"name": "hashicorp_vault",
|
||||
"url": "vault-server.mydomain.com",
|
||||
"salt": "<base64 encoded(<32 bytes long key used to hash the crypted data)>",
|
||||
"url": "vault-server.my-domain.com",
|
||||
"salt": "<base64 encoded(<32 bytes long key used to hash the encrypted data)>",
|
||||
"port": 443,
|
||||
"root_path": "vmware_daemon",
|
||||
"role_id": "<base64 encoded(<iv><hmac_hash><encrypted_role_id>)",
|
||||
|
|
|
@ -25,6 +25,11 @@ use warnings;
|
|||
use Data::Dumper;
|
||||
use centreon::plugins::http;
|
||||
use JSON::XS;
|
||||
use MIME::Base64;
|
||||
use Crypt::OpenSSL::AES;
|
||||
use centreon::plugins::statefile;
|
||||
|
||||
my $VAULT_PATH_REGEX = qr/^secret::hashicorp_vault::([^:]+)::(.+)$/;
|
||||
|
||||
sub new {
|
||||
my ($class, %options) = @_;
|
||||
|
@ -32,21 +37,29 @@ sub new {
|
|||
bless $self, $class;
|
||||
|
||||
if (!defined($options{output})) {
|
||||
print "Class PasswordMgr: Need to specify 'output' argument.\n";
|
||||
print "Class PasswordMgr needs an 'output' argument that must be of type centreon::plugins::output.\n";
|
||||
exit 3;
|
||||
}
|
||||
if (!defined($options{options})) {
|
||||
$options{output}->add_option_msg(short_msg => "Class PasswordMgr: Need to specify 'options' argument.");
|
||||
print "Class PasswordMgr needs an 'options' argument that must be of type centreon::plugins::options.\n";
|
||||
$options{output}->option_exit();
|
||||
}
|
||||
|
||||
$options{options}->add_options(arguments => {
|
||||
'vault-config:s' => { name => 'vault_config', default => '/etc/centreon-engine/centreonvault.json'},
|
||||
'vault-config:s' => { name => 'vault_config', default => '/etc/centreon-engine/centreonvault.json'},
|
||||
'vault-cache:s' => { name => 'vault_cache', default => '/var/lib/centreon/centplugins/centreonvault_session'},
|
||||
'vault-env-file:s' => { name => 'vault_env_file', default => '/usr/share/centreon/.env'},
|
||||
});
|
||||
|
||||
$options{options}->add_help(package => __PACKAGE__, sections => 'VAULT OPTIONS');
|
||||
|
||||
$self->{output} = $options{output};
|
||||
$self->{http} = centreon::plugins::http->new(%options, noptions => 1, default_backend => 'curl');
|
||||
|
||||
# to access the vault, http protocol management is needed
|
||||
$self->{http} = centreon::plugins::http->new(%options, noptions => 1, default_backend => 'curl', insecure => 1);
|
||||
|
||||
# to store the token and its expiration date, a statefile is needed
|
||||
$self->{cache} = centreon::plugins::statefile->new();
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
@ -56,20 +69,21 @@ sub extract_map_options {
|
|||
|
||||
$self->{map_option} = [];
|
||||
|
||||
# Parse all options to find '/\{.*\:\:secret\:\:(.*)\}/' dedicated patern in value and add entries in map_option
|
||||
# Parse all options to find '/# .*\:\:secret\:\:(.*)/' pattern in the options values and add entries in map_option
|
||||
foreach my $option (keys %{$options{option_results}}) {
|
||||
if (defined($options{option_results}{$option})) {
|
||||
next if ($option eq 'map_option');
|
||||
if (ref($options{option_results}{$option}) eq 'ARRAY') {
|
||||
foreach (@{$options{option_results}{$option}}) {
|
||||
if ($_ =~ /\{.*\:\:secret\:\:(.*)\:\:(.*)\}/i) {
|
||||
push (@{$self->{request_endpoint}}, "$1::/v1/".$2);
|
||||
if ($_ =~ $VAULT_PATH_REGEX) {
|
||||
push (@{$self->{request_endpoint}}, "/v1/$1::$2");
|
||||
push (@{$self->{map_option}}, $option."=%".$_);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($options{option_results}{$option} =~ /\{.*\:\:secret\:\:(.*)\:\:(.*)\}/i) {
|
||||
push (@{$self->{request_endpoint}}, "$1::/v1/".$2);
|
||||
|
||||
if (my ($path, $secret) = $options{option_results}{$option} =~ $VAULT_PATH_REGEX) {
|
||||
push (@{$self->{request_endpoint}}, "/v1/" . $path . "::" . $secret);
|
||||
push (@{$self->{map_option}}, $option."=%".$options{option_results}{$option});
|
||||
}
|
||||
}
|
||||
|
@ -80,95 +94,280 @@ sub extract_map_options {
|
|||
sub vault_settings {
|
||||
my ($self, %options) = @_;
|
||||
|
||||
if (!defined($options{option_results}->{vault_config})
|
||||
|| $options{option_results}->{vault_config} eq '') {
|
||||
$self->{output}->add_option_msg(short_msg => "Please set --vault-config option");
|
||||
if (centreon::plugins::misc::is_empty($options{option_results}->{vault_config})) {
|
||||
$self->{output}->add_option_msg(short_msg => "Please provide a Centreon Vault configuration file path with --vault-config option");
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
if (! -f $options{option_results}->{vault_config}) {
|
||||
$self->{output}->add_option_msg(short_msg => "Cannot find file '$options{option_results}->{vault_config}'");
|
||||
$self->{output}->add_option_msg(short_msg => "File '$options{option_results}->{vault_config}' could not be found.");
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
|
||||
$self->{vault_cache} = $options{option_results}->{vault_cache};
|
||||
$self->{vault_env_file} = $options{option_results}->{vault_env_file};
|
||||
$self->{vault_config} = $options{option_results}->{vault_config};
|
||||
|
||||
|
||||
my $file_content = do {
|
||||
local $/ = undef;
|
||||
if (!open my $fh, "<", $options{option_results}->{vault_config}) {
|
||||
$self->{output}->add_option_msg(short_msg => "Could not open file $options{option_results}->{vault_config}: $!");
|
||||
$self->{output}->add_option_msg(short_msg => "Could not read file $options{option_results}->{vault_config}: $!");
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
<$fh>;
|
||||
};
|
||||
|
||||
my $json;
|
||||
eval {
|
||||
$json = JSON::XS->new->utf8->decode($file_content);
|
||||
};
|
||||
if ($@) {
|
||||
$self->{output}->add_option_msg(short_msg => "Cannot decode json file");
|
||||
# decode the JSON content of the file
|
||||
my $json = centreon::plugins::misc::json_decode($file_content);
|
||||
if (!defined($json)) {
|
||||
$self->{output}->add_option_msg(short_msg => "Cannot decode JSON : $file_content\n");
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
|
||||
foreach my $vault_name (keys %$json) {
|
||||
$self->{$vault_name}->{vault_protocol} = 'https';
|
||||
$self->{$vault_name}->{vault_address} = '127.0.0.1';
|
||||
$self->{$vault_name}->{vault_port} = '8100';
|
||||
# set the default values
|
||||
$self->{vault}->{protocol} = 'https';
|
||||
$self->{vault}->{url} = '127.0.0.1';
|
||||
$self->{vault}->{port} = '8100';
|
||||
|
||||
$self->{$vault_name}->{vault_protocol} = $json->{$vault_name}->{'vault-protocol'}
|
||||
if ($json->{$vault_name}->{'vault-protocol'} && $json->{$vault_name}->{'vault-protocol'} ne '');
|
||||
$self->{$vault_name}->{vault_address} = $json->{$vault_name}->{'vault-address'}
|
||||
if ($json->{$vault_name}->{'vault-address'} && $json->{$vault_name}->{'vault-address'} ne '');
|
||||
$self->{$vault_name}->{vault_port} = $json->{$vault_name}->{'vault-port'}
|
||||
if ($json->{$vault_name}->{'vault-port'} && $json->{$vault_name}->{'vault-port'} ne '');
|
||||
$self->{$vault_name}->{vault_token} = $json->{$vault_name}->{'vault-token'}
|
||||
if ($json->{$vault_name}->{'vault-token'} && $json->{$vault_name}->{'vault-token'} ne '');
|
||||
# define the list of expected attributes in the JSON file
|
||||
my @valid_json_options = (
|
||||
'protocol',
|
||||
'url',
|
||||
'port',
|
||||
'root_path',
|
||||
'token',
|
||||
'secret_id',
|
||||
'role_id'
|
||||
);
|
||||
|
||||
# set the object fields when the json fields are not empty
|
||||
foreach my $valid_option (@valid_json_options) {
|
||||
$self->{vault}->{$valid_option} = $json->{$valid_option}
|
||||
if ( !centreon::plugins::misc::is_empty( $json->{ $valid_option } ) );
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_decryption_key {
|
||||
my ($self, %options) = @_;
|
||||
|
||||
# try getting APP_SECRET from the environment variables
|
||||
if ( !centreon::plugins::misc::is_empty($ENV{'APP_SECRET'}) ) {
|
||||
return $ENV{'APP_SECRET'};
|
||||
}
|
||||
|
||||
# try getting APP_SECRET defined in the env file (default: /usr/share/centreon/.env) file
|
||||
my $fh;
|
||||
return undef if (!open $fh, "<", $self->{vault_env_file});
|
||||
for my $line (<$fh>) {
|
||||
if ($line =~ /^APP_SECRET=(.*)$/) {
|
||||
return $1;
|
||||
}
|
||||
}
|
||||
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub extract_and_decrypt {
|
||||
my ($self, %options) = @_;
|
||||
|
||||
my $input = decode_base64($options{data});
|
||||
my $key = $options{key};
|
||||
|
||||
# with AES-256, the IV length must 16 bytes
|
||||
my $iv_length = 16;
|
||||
# extract the IV, the hashed data, the encrypted data
|
||||
my $iv = substr($input, 0, $iv_length); # initialization vector
|
||||
my $hashed_data = substr($input, $iv_length, 64); # hmac of the original data, for integrity control
|
||||
my $encrypted_data = substr($input, $iv_length + 64); # data to decrypt
|
||||
|
||||
# Creating the AES decryption object
|
||||
my $cipher;
|
||||
eval {
|
||||
$cipher = Crypt::OpenSSL::AES->new(
|
||||
$key,
|
||||
{
|
||||
'cipher' => 'AES-256-CBC',
|
||||
'iv' => $iv
|
||||
}
|
||||
);
|
||||
};
|
||||
if ($@) {
|
||||
$self->{output}->add_option_msg(short_msg => "There was an error while creating the AES object: " . $@);
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
|
||||
# Decrypting the data
|
||||
my $decrypted_data;
|
||||
eval {$decrypted_data = $cipher->decrypt($encrypted_data);};
|
||||
if ($@) {
|
||||
$self->{output}->add_option_msg(short_msg => "There was an error while decrypting an AES-encrypted data: " . $@);
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
|
||||
return $decrypted_data;
|
||||
}
|
||||
|
||||
sub is_token_still_valid {
|
||||
my ($self) = @_;
|
||||
if (
|
||||
!defined($self->{auth})
|
||||
|| centreon::plugins::misc::is_empty($self->{auth}->{token})
|
||||
|| centreon::plugins::misc::is_empty($self->{auth}->{expiration_epoch})
|
||||
|| $self->{auth}->{expiration_epoch} !~ /\d+/
|
||||
|| $self->{auth}->{expiration_epoch} <= time()
|
||||
) {
|
||||
$self->{output}->output_add(long_msg => "The token is missing or has expired or is invalid.", debug => 1);
|
||||
return undef;
|
||||
}
|
||||
$self->{output}->output_add(long_msg => "The cached token is still valid.", debug => 1);
|
||||
# Possible enhancement: check the token validity by calling this endpoint: /v1/auth/token/lookup-self
|
||||
# {"request_id":"XXXXX","lease_id":"","renewable":false,"lease_duration":0,"data":{"accessor":"XXXXXXX","creation_time":1732294406,"creation_ttl":2764800,"display_name":"approle","entity_id":"XXX","expire_time":"2024-12-24T16:53:26.932122122Z","explicit_max_ttl":0,"id":"hvs.secretToken","issue_time":"2024-11-22T16:53:26.932129132Z","meta":{"role_name":"myvault"},"num_uses":0,"orphan":true,"path":"auth/approle/login","policies":["default","myvault"],"renewable":true,"ttl":2764724,"type":"service"},"wrap_info":null,"warnings":null,"auth":null,"mount_type":"token"}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub check_authentication {
|
||||
my ($self, %options) = @_;
|
||||
|
||||
# prepare the cache (aka statefile)
|
||||
$self->{cache}->check_options(option_results => $options{option_results});
|
||||
my ($dir, $file, $suffix) = $options{option_results}->{vault_cache} =~ /^(.*\/)([^\/]+)(_.*)?$/;
|
||||
|
||||
# Try reading the cache file
|
||||
if ($self->{cache}->read(
|
||||
statefile => $file,
|
||||
statefile_suffix => defined($suffix) ? $suffix : '',
|
||||
statefile_dir => $dir,
|
||||
statefile_format => 'json'
|
||||
)) {
|
||||
# if the cache file could be read, get the token and its expiration date
|
||||
$self->{auth} = {
|
||||
token => $self->{cache}->get(name => 'token'),
|
||||
expiration_epoch => $self->{cache}->get(name => 'expiration_epoch')
|
||||
};
|
||||
}
|
||||
|
||||
# if it is not valid, authenticate to get a new token
|
||||
if ( !$self->is_token_still_valid() ) {
|
||||
return $self->authenticate();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub authenticate {
|
||||
my ($self) = @_;
|
||||
|
||||
# initial value: assuming the role and secret id might not be encrypted
|
||||
my $role_id = $self->{vault}->{role_id};
|
||||
my $secret_id = $self->{vault}->{secret_id};
|
||||
if (centreon::plugins::misc::is_empty($role_id) || centreon::plugins::misc::is_empty($secret_id)) {
|
||||
$self->{output}->add_option_msg(short_msg => "Unable to authenticate to the vault: role_id or secret_id is empty.");
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
my $decryption_key = $self->get_decryption_key();
|
||||
|
||||
# Decrypt the role_id and the secret_id if we have a decryption key
|
||||
if ( !centreon::plugins::misc::is_empty($decryption_key) ) {
|
||||
$role_id = $self->extract_and_decrypt(
|
||||
data => $role_id,
|
||||
key => $decryption_key
|
||||
);
|
||||
$secret_id = $self->extract_and_decrypt(
|
||||
data => $secret_id,
|
||||
key => $decryption_key
|
||||
);
|
||||
}
|
||||
|
||||
# Authenticate to get the token
|
||||
my ($auth_result_json) = $self->{http}->request(
|
||||
hostname => $self->{vault}->{url},
|
||||
port => $self->{vault}->{port},
|
||||
proto => $self->{vault}->{protocol},
|
||||
method => 'POST',
|
||||
url_path => "/v1/auth/approle/login",
|
||||
query_form_post => "role_id=$role_id&secret_id=$secret_id",
|
||||
header => [
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
'Accept: */*',
|
||||
'X-Vault-Request: true',
|
||||
'User-Agent: Centreon-Plugins'
|
||||
]
|
||||
);
|
||||
|
||||
# Convert the response into a JSON object
|
||||
my $auth_result_obj = centreon::plugins::misc::json_decode($auth_result_json);
|
||||
if (!defined($auth_result_obj)) {
|
||||
# exit with UNKNOWN status
|
||||
$self->{output}->add_option_msg(short_msg => "Cannot decode JSON response from the vault server: $auth_result_json.");
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
# Authentication to the vault has passed
|
||||
# store the token (.auth.client_token) and its expiration date (current date + .lease_duration)
|
||||
my $expiration_epoch = -1;
|
||||
my $lease_duration = $auth_result_obj->{auth}->{lease_duration};
|
||||
if ( defined($lease_duration)
|
||||
&& $lease_duration =~ /\d+/
|
||||
&& $lease_duration > 0 ) {
|
||||
$expiration_epoch = time() + $lease_duration;
|
||||
}
|
||||
$self->{auth} = {
|
||||
'token' => $auth_result_obj->{auth}->{client_token},
|
||||
'expiration_epoch' => $expiration_epoch
|
||||
};
|
||||
$self->{cache}->write(data => $self->{auth}, name => 'auth');
|
||||
|
||||
$self->{output}->output_add(long_msg => "Authenticating worked. Token valid until "
|
||||
. localtime($self->{auth}->{expiration_epoch}), debug => 1);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub request_api {
|
||||
my ($self, %options) = @_;
|
||||
|
||||
$self->vault_settings(%options);
|
||||
|
||||
# check the authentication
|
||||
if (!$self->check_authentication(%options)) {
|
||||
$self->{output}->add_option_msg(short_msg => "Unable to authenticate to the vault.");
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
|
||||
$self->{lookup_values} = {};
|
||||
|
||||
foreach my $item (@{$self->{request_endpoint}}) {
|
||||
# Extract vault name configuration from endpoint
|
||||
# 'vault::/v1/<root_path>/monitoring/hosts/7ad55afc-fa9e-4851-85b7-e26f47e421d7'
|
||||
my ($vault_name, $endpoint);
|
||||
if ($item =~ /(.*)\:\:(.*)/i) {
|
||||
$vault_name = $1;
|
||||
$endpoint = $2;
|
||||
}
|
||||
my ($endpoint, $secret) = $item =~ /^(.*)\:\:(.*)$/;
|
||||
|
||||
if (!defined($self->{$vault_name})) {
|
||||
$self->{output}->add_option_msg(short_msg => "Cannot get vault access for: $vault_name");
|
||||
$self->{output}->option_exit();
|
||||
}
|
||||
|
||||
my $headers = ['Accept: application/json'];
|
||||
if (defined($self->{$vault_name}->{vault_token})) {
|
||||
push @$headers, 'X-Vault-Token: ' . $self->{$vault_name}->{vault_token};
|
||||
}
|
||||
|
||||
my ($response) = $self->{http}->request(
|
||||
hostname => $self->{$vault_name}->{vault_address},
|
||||
port => $self->{$vault_name}->{vault_port},
|
||||
proto => $self->{$vault_name}->{vault_protocol},
|
||||
hostname => $self->{vault}->{url},
|
||||
port => $self->{vault}->{port},
|
||||
proto => $self->{vault}->{protocol},
|
||||
method => 'GET',
|
||||
url_path => $endpoint,
|
||||
header => $headers
|
||||
header => [
|
||||
'Accept: application/json',
|
||||
'User-Agent: Centreon-Plugins',
|
||||
'X-Vault-Request: true',
|
||||
'X-Vault-Token: ' . $self->{auth}->{token}
|
||||
]
|
||||
);
|
||||
|
||||
my $json;
|
||||
eval {
|
||||
$json = JSON::XS->new->utf8->decode($response);
|
||||
};
|
||||
if ($@) {
|
||||
$self->{output}->add_option_msg(short_msg => "Cannot decode Vault JSON response: $@");
|
||||
|
||||
my $json = centreon::plugins::misc::json_decode($response);
|
||||
if (!defined($json->{data})) {
|
||||
$self->{output}->add_option_msg(short_msg => "Cannot decode Vault JSON response: $response");
|
||||
$self->{output}->option_exit();
|
||||
};
|
||||
|
||||
foreach (keys %{$json->{data}}) {
|
||||
$self->{lookup_values}->{'{' . $_ . '::secret::' . $vault_name . '::' . substr($endpoint, index($endpoint, '/', 1) + 1) . '}'} = $json->{data}->{$_};
|
||||
foreach my $secret_name (keys %{$json->{data}->{data}}) {
|
||||
# e.g. secret::hashicorp_vault::myspace/data/snmp::PubCommunity
|
||||
$self->{lookup_values}->{'secret::hashicorp_vault::' . substr($endpoint, index($endpoint, '/', 1) + 1) . '::' . $secret_name} = $json->{data}->{data}->{$secret_name};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -176,16 +375,11 @@ sub request_api {
|
|||
sub do_map {
|
||||
my ($self, %options) = @_;
|
||||
|
||||
foreach (@{$self->{map_option}}) {
|
||||
next if (! /^(.+?)=%(.+)$/);
|
||||
|
||||
my ($option, $map) = ($1, $2);
|
||||
|
||||
$map = $self->{lookup_values}->{$2} if (defined($self->{lookup_values}->{$2}));
|
||||
$option =~ s/-/_/g;
|
||||
$options{option_results}->{$option} = $map;
|
||||
foreach my $mapping (@{$self->{map_option}}) {
|
||||
my ($opt_name, $opt_value) = $mapping =~ /^(.+?)=%(.+)$/ or next;
|
||||
$opt_name =~ s/-/_/g;
|
||||
$options{option_results}->{$opt_name} = defined($self->{lookup_values}->{$opt_value}) ? $self->{lookup_values}->{$opt_value} : $opt_value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sub manage_options {
|
||||
|
@ -219,7 +413,15 @@ To be used with an array containing keys/values saved in a secret path by resour
|
|||
|
||||
=item B<--vault-config>
|
||||
|
||||
The path to the file defining access to the Centreon vault (/etc/centreon-engine/centreonvault.json by default)
|
||||
Path to the file defining access to the Centreon vault (default: C</etc/centreon-engine/centreonvault.json>).
|
||||
|
||||
=item B<--vault-cache>
|
||||
|
||||
Path to the file where the token to access the Centreon vault will be stored (default: C</var/lib/centreon/centplugins/centreonvault_session>).
|
||||
|
||||
=item B<--vault-env-file>
|
||||
|
||||
Path to the file containing the APP_SECRET variable (default: C</usr/share/centreon/.env>).
|
||||
|
||||
=back
|
||||
|
||||
|
@ -228,3 +430,129 @@ The path to the file defining access to the Centreon vault (/etc/centreon-engine
|
|||
B<centreonvault>.
|
||||
|
||||
=cut
|
||||
|
||||
=head1 NAME
|
||||
|
||||
centreon::plugins::passwordmgr::centreonvault - Module for getting secrets from Centreon Vault.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use centreon::plugins::passwordmgr::centreonvault;
|
||||
|
||||
my $vault = centreon::plugins::passwordmgr::centreonvault->new(output => $output, options => $options);
|
||||
$vault->manage_options(option_results => \%option_results);
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This module provides methods to retrieve secrets (passwords, SNMP communities, ...) from Centreon Vault (adequately
|
||||
configured HashiCorp Vault).
|
||||
It extracts and decrypt the information required to login to the vault from the vault configuration file, authenticates
|
||||
to the vault, retrieves secrets, and maps them to the corresponding options for the centreon-plugins to work with.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 new
|
||||
|
||||
my $vault = centreon::plugins::passwordmgr::centreonvault->new(%options);
|
||||
|
||||
Creates a new `centreon::plugins::passwordmgr::centreonvault` object. The `%options` hash can include:
|
||||
|
||||
=over 4
|
||||
|
||||
=item * output
|
||||
|
||||
The output object for displaying debug and error messages.
|
||||
|
||||
=item * options
|
||||
|
||||
The options object for handling command-line options.
|
||||
|
||||
=back
|
||||
|
||||
=head2 extract_map_options
|
||||
|
||||
$vault->extract_map_options(option_results => \%option_results);
|
||||
|
||||
Extracts and maps options that match the Vault path regex pattern (C</^secret::hashicorp_vault::([^:]+)::(.+)$/>). The
|
||||
`%option_results` hash should include the command-line options.
|
||||
|
||||
=head2 vault_settings
|
||||
|
||||
$vault->vault_settings(option_results => \%option_results);
|
||||
|
||||
Loads and validates the Vault configuration from the specified file.
|
||||
The `%option_results` hash should include the command-line options.
|
||||
|
||||
=head2 get_decryption_key
|
||||
|
||||
my $key = $vault->get_decryption_key();
|
||||
|
||||
Retrieves the decryption key from C<APP_SECRET> environment variable. It will look for it in the the specified
|
||||
environment file if it is not available in the environment variables.
|
||||
|
||||
=head2 extract_and_decrypt
|
||||
|
||||
my $decrypted_data = $vault->extract_and_decrypt(data => $data, key => $key);
|
||||
|
||||
Decrypts the given data using the specified key. The options must include:
|
||||
|
||||
=over 4
|
||||
|
||||
=item * data
|
||||
|
||||
The base64-encoded data to decrypt.
|
||||
|
||||
=item * key
|
||||
|
||||
The base64-encoded decryption key.
|
||||
|
||||
=back
|
||||
|
||||
=head2 is_token_still_valid
|
||||
|
||||
my $is_valid = $vault->is_token_still_valid();
|
||||
|
||||
Checks if there is a token in the cache and if it is still valid based on its expiration date. Returns 1 if valid, otherwise undef.
|
||||
|
||||
=head2 check_authentication
|
||||
|
||||
$vault->check_authentication(option_results => \%option_results);
|
||||
|
||||
Checks the authentication status and retrieves a new token if necessary. The `%option_results` hash should include the command-line options.
|
||||
|
||||
=head2 authenticate
|
||||
|
||||
$vault->authenticate();
|
||||
|
||||
Authenticates to the Vault, retrieves a new token and stores it in the dedicated cache file.
|
||||
|
||||
=head2 request_api
|
||||
|
||||
$vault->request_api(option_results => \%option_results);
|
||||
|
||||
Sends requests to the Vault API to retrieve secrets. The `%option_results` hash should include the command-line options.
|
||||
|
||||
=head2 do_map
|
||||
|
||||
$vault->do_map(option_results => \%option_results);
|
||||
|
||||
Maps the retrieved secrets to the corresponding options. The `%option_results` hash should include the command-line options.
|
||||
Calling this method will update the `%option_results` hash replacing vault paths with the retrieved secrets.
|
||||
|
||||
=head2 manage_options
|
||||
|
||||
$vault->manage_options(option_results => \%option_results);
|
||||
|
||||
Manages the options by extracting, requesting, and mapping secrets. The `%option_results` hash should include the command-line options.
|
||||
|
||||
NB: This is the main method to be called from outside the module. All other methods are intended to be used internally.
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Centreon
|
||||
|
||||
=head1 LICENSE
|
||||
|
||||
Licensed under the Apache License, Version 2.0.
|
||||
|
||||
=cut
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
{
|
||||
"uuid": "5beab5e3-1b32-4ec1-a6d9-18c546c2d894",
|
||||
"lastMigration": 33,
|
||||
"name": "Centreonvault",
|
||||
"endpointPrefix": "",
|
||||
"latency": 0,
|
||||
"port": 3000,
|
||||
"hostname": "",
|
||||
"folders": [],
|
||||
"routes": [
|
||||
{
|
||||
"uuid": "9623237f-f204-4972-ac46-8cfaadfa975a",
|
||||
"type": "http",
|
||||
"documentation": "",
|
||||
"method": "post",
|
||||
"endpoint": "v1/auth/approle/login",
|
||||
"responses": [
|
||||
{
|
||||
"uuid": "3bb67f80-c60a-41a9-b071-f5a559f19613",
|
||||
"body": "{\n \"request_id\": \"r2p2c3po-b013-723a-24c7-ad80aa1fbddb\",\n \"lease_id\": \"\",\n \"renewable\": false,\n \"lease_duration\": 0,\n \"data\": null,\n \"wrap_info\": null,\n \"warnings\": null,\n \"auth\": {\n \"client_token\": \"hvs.thistokenisafakeonebutwillworkwiththetests\",\n \"accessor\": \"7PjTD&rpX53oqLRNa4C5t\",\n \"policies\": [\n \"default\",\n \"centreon-plugins\"\n ],\n \"token_policies\": [\n \"default\",\n \"omercier\"\n ],\n \"metadata\": {\n \"role_name\": \"centreon-plugins\"\n },\n \"lease_duration\": 2764800,\n \"renewable\": true,\n \"entity_id\": \"bbdov2-0dd9-97e8-66d6-3db885ccffd8\",\n \"token_type\": \"service\",\n \"orphan\": true,\n \"mfa_requirement\": null,\n \"num_uses\": 0\n },\n \"mount_type\": \"\"\n}",
|
||||
"latency": 0,
|
||||
"statusCode": 200,
|
||||
"label": "",
|
||||
"headers": [
|
||||
{
|
||||
"key": "access-control-allow-headers",
|
||||
"value": "Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With"
|
||||
},
|
||||
{
|
||||
"key": "access-control-allow-methods",
|
||||
"value": "GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS"
|
||||
},
|
||||
{
|
||||
"key": "access-control-allow-origin",
|
||||
"value": "*"
|
||||
},
|
||||
{
|
||||
"key": "content-security-policy",
|
||||
"value": "default-src 'none'"
|
||||
},
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "text/html; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
}
|
||||
],
|
||||
"bodyType": "INLINE",
|
||||
"filePath": "",
|
||||
"databucketID": "",
|
||||
"sendFileAsBody": false,
|
||||
"rules": [
|
||||
{
|
||||
"target": "query",
|
||||
"modifier": "role_id",
|
||||
"value": "thisroleidisinplaintext",
|
||||
"invert": false,
|
||||
"operator": "equals"
|
||||
},
|
||||
{
|
||||
"target": "query",
|
||||
"modifier": "secret_id",
|
||||
"value": "thissecretidisinplaintext",
|
||||
"invert": false,
|
||||
"operator": "equals"
|
||||
}
|
||||
],
|
||||
"rulesOperator": "OR",
|
||||
"disableTemplating": false,
|
||||
"fallbackTo404": false,
|
||||
"default": false,
|
||||
"crudKey": "id",
|
||||
"callbacks": []
|
||||
},
|
||||
{
|
||||
"uuid": "bba1ccb5-9415-4630-a82e-ff192b1f5680",
|
||||
"body": "{\"errors\":[\"invalid role or secret ID\"]}",
|
||||
"latency": 0,
|
||||
"statusCode": 400,
|
||||
"label": "",
|
||||
"headers": [],
|
||||
"bodyType": "INLINE",
|
||||
"filePath": "",
|
||||
"databucketID": "",
|
||||
"sendFileAsBody": false,
|
||||
"rules": [],
|
||||
"rulesOperator": "OR",
|
||||
"disableTemplating": false,
|
||||
"fallbackTo404": false,
|
||||
"default": false,
|
||||
"crudKey": "id",
|
||||
"callbacks": []
|
||||
}
|
||||
],
|
||||
"responseMode": null,
|
||||
"streamingMode": null,
|
||||
"streamingInterval": 0
|
||||
},
|
||||
{
|
||||
"uuid": "5378cdb8-7126-4b58-aa23-ef79f5b06ba4",
|
||||
"type": "http",
|
||||
"documentation": "",
|
||||
"method": "get",
|
||||
"endpoint": "v1/myvault/data/snmp",
|
||||
"responses": [
|
||||
{
|
||||
"uuid": "ca08be91-0f07-42e1-8119-c2510c1b31f4",
|
||||
"body": "{\"request_id\":\"bbdo2cbd-e3f0-d84c-b668-65416f0b9d97\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":0,\"data\":{\"data\":{\"Linux\":\"os/linux/snmp/linux\"},\"metadata\":{\"created_time\":\"2024-11-21T12:34:26.606125626Z\",\"custom_metadata\":null,\"deletion_time\":\"\",\"destroyed\":false,\"version\":1}},\"wrap_info\":null,\"warnings\":null,\"auth\":null,\"mount_type\":\"kv\"}\n",
|
||||
"latency": 0,
|
||||
"statusCode": 200,
|
||||
"label": "",
|
||||
"headers": [],
|
||||
"bodyType": "INLINE",
|
||||
"filePath": "",
|
||||
"databucketID": "",
|
||||
"sendFileAsBody": false,
|
||||
"rules": [
|
||||
{
|
||||
"target": "header",
|
||||
"modifier": "X-Vault-Token",
|
||||
"value": "hvs.thistokenisafakeonebutwillworkwiththetests",
|
||||
"invert": false,
|
||||
"operator": "equals"
|
||||
}
|
||||
],
|
||||
"rulesOperator": "OR",
|
||||
"disableTemplating": false,
|
||||
"fallbackTo404": false,
|
||||
"default": false,
|
||||
"crudKey": "id",
|
||||
"callbacks": []
|
||||
},
|
||||
{
|
||||
"uuid": "9bab45de-a545-4863-a8f6-7613a1d2ad64",
|
||||
"latency": 0,
|
||||
"statusCode": 404,
|
||||
"label": "",
|
||||
"headers": [
|
||||
{
|
||||
"key": "access-control-allow-headers",
|
||||
"value": "Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With"
|
||||
},
|
||||
{
|
||||
"key": "access-control-allow-methods",
|
||||
"value": "GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS"
|
||||
},
|
||||
{
|
||||
"key": "access-control-allow-origin",
|
||||
"value": "*"
|
||||
},
|
||||
{
|
||||
"key": "content-security-policy",
|
||||
"value": "default-src 'none'"
|
||||
},
|
||||
{
|
||||
"key": "content-type",
|
||||
"value": "text/html; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
}
|
||||
],
|
||||
"bodyType": "INLINE",
|
||||
"filePath": "",
|
||||
"databucketID": "",
|
||||
"sendFileAsBody": false,
|
||||
"rules": [],
|
||||
"rulesOperator": "OR",
|
||||
"disableTemplating": false,
|
||||
"fallbackTo404": false,
|
||||
"default": true,
|
||||
"crudKey": "id",
|
||||
"callbacks": []
|
||||
}
|
||||
],
|
||||
"responseMode": null,
|
||||
"streamingMode": null,
|
||||
"streamingInterval": 0
|
||||
}
|
||||
],
|
||||
"rootChildren": [
|
||||
{
|
||||
"type": "route",
|
||||
"uuid": "9623237f-f204-4972-ac46-8cfaadfa975a"
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"uuid": "5378cdb8-7126-4b58-aa23-ef79f5b06ba4"
|
||||
}
|
||||
],
|
||||
"proxyMode": false,
|
||||
"proxyHost": "",
|
||||
"proxyRemovePrefix": false,
|
||||
"tlsOptions": {
|
||||
"enabled": false,
|
||||
"type": "CERT",
|
||||
"pfxPath": "",
|
||||
"certPath": "",
|
||||
"keyPath": "",
|
||||
"caPath": "",
|
||||
"passphrase": ""
|
||||
},
|
||||
"cors": true,
|
||||
"headers": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Methods",
|
||||
"value": "GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Headers",
|
||||
"value": "Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With"
|
||||
}
|
||||
],
|
||||
"proxyReqHeaders": [
|
||||
{
|
||||
"key": "",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"proxyResHeaders": [
|
||||
{
|
||||
"key": "",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"data": [],
|
||||
"callbacks": []
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
APP_SECRET=thiskeyismadefortestspurpose1234
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "hashicorp_vault",
|
||||
"url": "127.0.0.1",
|
||||
"protocol": "http",
|
||||
"salt": "controlhmachashingkeyforcontrols",
|
||||
"port": 3000,
|
||||
"root_path": "",
|
||||
"role_id": "FO7/tbXY90gg+9igMXhLI2BYrGiRxOtmsgY8GlSVO0DHTO0DYGFnExCAuqPyVLoyvvjdba7Crl7TOb73H/QGMlFkfbE4/qAeiTF9ReMS1TnsUoxfTLKqCXERXGApkxOrSIhv/z6+UBKmQwPVhLwD7w==",
|
||||
"secret_id": "odQ16TYwubSSi/m4mo88D8Trupbf+3ehgmfxA7wrEtbCEDciZciBRY2cc9Yb2yD+ivR64WR8RsPVPpGbPj1AFpu1TIwL88ic7zjGZaZiIr5kZenoi6xJquxQZbNW5t2N8JaUb/Qupp2wvQ2rxv9zoQ=="
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "hashicorp_vault",
|
||||
"url": "127.0.0.1",
|
||||
"salt": "cetteclenestpasutiliseeinthetest",
|
||||
"port": 3001,
|
||||
"root_path": "",
|
||||
"role_id": "",
|
||||
"secret_id": ""
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "hashicorp_vault",
|
||||
"url": "127.0.0.1",
|
||||
"protocol": "http",
|
||||
"salt": "",
|
||||
"port": 3000,
|
||||
"root_path": "",
|
||||
"role_id": "thisroleidisinplaintext",
|
||||
"secret_id": "thissecretidisinplaintext"
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
*** Settings ***
|
||||
Documentation Centreonvault module
|
||||
|
||||
Resource ${CURDIR}${/}..${/}..${/}..${/}resources/import.resource
|
||||
|
||||
Suite Setup Start Mockoon ${MOCKOON_JSON}
|
||||
Suite Teardown Stop Mockoon
|
||||
Test Timeout 120s
|
||||
|
||||
|
||||
*** Variables ***
|
||||
|
||||
${CMD} ${CENTREON_PLUGINS} --plugin=os::linux::snmp::plugin --pass-manager=centreonvault --snmp-port=${SNMPPORT} --snmp-version=${SNMPVERSION} --hostname=${HOSTNAME}
|
||||
${VAULT_CACHE} /var/lib/centreon/centplugins/centreonvault_cache
|
||||
${VAULT_FILES} ${CURDIR}${/}..${/}..${/}..${/}centreon${/}plugins${/}passwordmgr
|
||||
${MOCKOON_JSON} ${VAULT_FILES}${/}centreonvault.mockoon.json
|
||||
|
||||
*** Test Cases ***
|
||||
Linux Memory with vault ${tc}
|
||||
[Tags] snmp linux vault mockoon
|
||||
Remove File ${VAULT_CACHE}
|
||||
${command} Catenate
|
||||
... ${CMD}
|
||||
... --mode=memory
|
||||
... --snmp-community=secret::hashicorp_vault::myvault/data/snmp::${secret}
|
||||
... --vault-config=${vault_config}
|
||||
... --vault-cache=${VAULT_CACHE}
|
||||
... ${extra_options}
|
||||
|
||||
Ctn Run Command And Check Result As Regexp ${command} ${expected_regexp}
|
||||
|
||||
Examples: tc secret vault_config extra_options expected_regexp --
|
||||
... 1 Linux ${EMPTY} ${EMPTY} UNKNOWN: Please provide a Centreon Vault configuration file path with --vault-config option
|
||||
... 2 Linux ${VAULT_FILES}${/}vault.json ${EMPTY} UNKNOWN: File '.*/centreon/plugins/passwordmgr/vault.json' could not be found.
|
||||
... 3 Linux ${VAULT_FILES}${/}vault_config_incomplete.json ${EMPTY} UNKNOWN: Unable to authenticate to the vault: role_id or secret_id is empty.
|
||||
... 4 Linux ${VAULT_FILES}${/}vault_config_plain.json --debug OK: Ram Total: 1.92 GB Used
|
||||
... 5 Linux ${VAULT_FILES}${/}vault_config_encrypted.json --vault-env-file=${VAULT_FILES}${/}env OK: Ram Total: 1.92 GB Used
|
|
@ -28,10 +28,13 @@ aws
|
|||
AWSCLI
|
||||
--aws-role-arn
|
||||
Backbox
|
||||
base64
|
||||
blocked-by-uf
|
||||
--cacert-file
|
||||
cardtemperature
|
||||
centreon
|
||||
Centreon
|
||||
centreonvault
|
||||
--cert-pkcs12
|
||||
--cert-pwd
|
||||
CloudWatch
|
||||
|
@ -66,6 +69,7 @@ dfsrevent
|
|||
dns-resolve-time
|
||||
--dyn-mode
|
||||
-EncodedCommand
|
||||
env
|
||||
ESX
|
||||
eth
|
||||
--exclude-fs
|
||||
|
|
Loading…
Reference in New Issue