Merge pull request #5571 from Icinga/feature/ca-proxy

Implement support for forwarding certificate signing requests in the cluster
This commit is contained in:
Gunnar Beutner 2017-09-12 14:00:59 +02:00 committed by GitHub
commit cd31327f72
40 changed files with 2193 additions and 729 deletions

View File

@ -2155,7 +2155,7 @@ Rephrased: If the parent service object changes into the `Warning` state, this
dependency will fail and render all child objects (hosts or services) unreachable.
You can determine the child's reachability by querying the `is_reachable` attribute
in for example [DB IDO](23-appendix.md#schema-db-ido-extensions).
in for example [DB IDO](24-appendix.md#schema-db-ido-extensions).
### Implicit Dependencies for Services on Host <a id="dependencies-implicit-host-service"></a>

View File

@ -189,53 +189,38 @@ The setup wizard will ensure that the following steps are taken:
* Enable the `api` feature.
* Generate a new certificate authority (CA) in `/var/lib/icinga2/ca` if it doesn't exist.
* Create a certificate signing request (CSR) for the local node.
* Sign the CSR with the local CA and copy all files to the `/etc/icinga2/pki` directory.
* Update the `zones.conf` file with the new zone hierarchy.
* Update `/etc/icinga2/features-enabled/api.conf` and `constants.conf`.
* Create a certificate for this node signed by the CA key.
* Update the [zones.conf](04-configuring-icinga-2.md#zones-conf) file with the new zone hierarchy.
* Update the [ApiListener](06-distributed-monitoring.md#distributed-monitoring-apilistener) and [constants](04-configuring-icinga-2.md#constants-conf) configuration.
Here is an example of a master setup for the `icinga2-master1.localdomain` node on CentOS 7:
[root@icinga2-master1.localdomain /]# icinga2 node wizard
Welcome to the Icinga 2 Setup Wizard!
```
[root@icinga2-master1.localdomain /]# icinga2 node wizard
We'll guide you through all required configuration details.
Welcome to the Icinga 2 Setup Wizard!
Please specify if this is a satellite setup ('n' installs a master setup) [Y/n]: n
Starting the Master setup routine...
Please specify the common name (CN) [icinga2-master1.localdomain]: icinga2-master1.localdomain
Checking for existing certificates for common name 'icinga2-master1.localdomain'...
Certificates not yet generated. Running 'api setup' now.
information/cli: Generating new CA.
information/base: Writing private key to '/var/lib/icinga2/ca/ca.key'.
information/base: Writing X509 certificate to '/var/lib/icinga2/ca/ca.crt'.
information/cli: Generating new CSR in '/etc/icinga2/pki/icinga2-master1.localdomain.csr'.
information/base: Writing private key to '/etc/icinga2/pki/icinga2-master1.localdomain.key'.
information/base: Writing certificate signing request to '/etc/icinga2/pki/icinga2-master1.localdomain.csr'.
information/cli: Signing CSR with CA and writing certificate to '/etc/icinga2/pki/icinga2-master1.localdomain.crt'.
information/cli: Copying CA certificate to '/etc/icinga2/pki/ca.crt'.
Generating master configuration for Icinga 2.
information/cli: Adding new ApiUser 'root' in '/etc/icinga2/conf.d/api-users.conf'.
information/cli: Enabling the 'api' feature.
Enabling feature api. Make sure to restart Icinga 2 for these changes to take effect.
information/cli: Dumping config items to file '/etc/icinga2/zones.conf'.
information/cli: Created backup file '/etc/icinga2/zones.conf.orig'.
Please specify the API bind host/port (optional):
Bind Host []:
Bind Port []:
information/cli: Created backup file '/etc/icinga2/features-available/api.conf.orig'.
information/cli: Updating constants.conf.
information/cli: Created backup file '/etc/icinga2/constants.conf.orig'.
information/cli: Updating constants file '/etc/icinga2/constants.conf'.
information/cli: Updating constants file '/etc/icinga2/constants.conf'.
information/cli: Updating constants file '/etc/icinga2/constants.conf'.
Done.
We will guide you through all required configuration details.
Now restart your Icinga 2 daemon to finish the installation!
Please specify if this is a satellite/client setup ('n' installs a master setup) [Y/n]: n
[root@icinga2-master1.localdomain /]# systemctl restart icinga2
Starting the Master setup routine...
As you can see, the CA public and private key are stored in the `/var/lib/icinga2/ca` directory.
Please specify the common name (CN) [icinga2-master1.localdomain]: icinga2-master1.localdomain
Reconfiguring Icinga...
Checking for existing certificates for common name 'master1'...
Generating master configuration for Icinga 2.
Please specify the API bind host/port (optional):
Bind Host []:
Bind Port []:
Done.
Now restart your Icinga 2 daemon to finish the installation!
```
You can verify that the CA public and private keys are stored in the `/var/lib/icinga2/ca` directory.
Keep this path secure and include it in your [backups](02-getting-started.md#install-backup).
In case you lose the CA private key you have to generate a new CA for signing new client
@ -246,23 +231,53 @@ Once the master setup is complete, you can also use this node as primary [CSR au
master. The following section will explain how to use the CLI commands in order to fetch their
signed certificate from this master node.
## Client/Satellite Setup <a id="distributed-monitoring-setup-satellite-client"></a>
## Signing Certificates on the Master <a id="distributed-monitoring-setup-sign-certificates-master"></a>
This section describes the setup of a satellite and/or client connected to an
existing master node setup. If you haven't done so already, please [run the master setup](06-distributed-monitoring.md#distributed-monitoring-setup-master).
All certificates must be signed by the same certificate authority (CA). This ensures
that all nodes trust each other in a distributed monitoring environment.
Icinga 2 on the master node must be running and accepting connections on port `5665`.
This CA is generated during the [master setup](06-distributed-monitoring.md#distributed-monitoring-setup-master)
and should be the same on all master instances.
You can avoid signing and deploying certificates [manually](#06-distributed-monitoring.md#distributed-monitoring-advanced-hints-certificates)
by using built-in methods for auto-signing certificate signing requests (CSR):
* [CSR Auto-Signing](06-distributed-monitoring.md#distributed-monitoring-setup-csr-auto-signing) which uses a client ticket generated on the master as trust identifier.
* [On-Demand CSR Signing](06-distributed-monitoring.md#distributed-monitoring-setup-on-demand-csr-signing) which allows to sign pending certificate requests on the master.
Both methods are described in detail below.
> **Note**
>
> [On-Demand CSR Signing](06-distributed-monitoring.md#distributed-monitoring-setup-on-demand-csr-signing) is available in Icinga 2 v2.8+.
### CSR Auto-Signing <a id="distributed-monitoring-setup-csr-auto-signing"></a>
The `node wizard` command will set up a satellite/client using CSR auto-signing. This
involves that the setup wizard sends a certificate signing request (CSR) to the
master node.
There is a security mechanism in place which requires the client to send in a valid
ticket for CSR auto-signing.
A client which sends a certificate signing request (CSR) must authenticate itself
in a trusted way. The master generates a client ticket which is included in this request.
That way the master can verify that the request matches the previously trusted ticket
and sign the request.
This ticket must be generated beforehand. The `ticket_salt` attribute for the [ApiListener](09-object-types.md#objecttype-apilistener)
must be configured in order to make this work.
> **Note**
>
> Icinga 2 v2.8 adds the possibility to forward signing requests on a satellite
> to the master node. This helps with the setup of [three level clusters](#06-distributed-monitoring.md#distributed-monitoring-scenarios-master-satellite-client)
> and more.
Advantages:
* Nodes can be installed by different users who have received the client ticket.
* No manual interaction necessary on the master node.
* Automation tools like Puppet, Ansible, etc. can retrieve the pre-generated ticket in their client catalog
and run the node setup directly.
Disadvantages:
* Tickets need to be generated on the master and copied to client setup wizards.
* No central signing management.
Setup wizards for satellite/client nodes will ask you for this specific client ticket.
There are two possible ways to retrieve the ticket:
@ -280,7 +295,7 @@ The following example shows how to generate a ticket on the master node `icinga2
[root@icinga2-master1.localdomain /]# icinga2 pki ticket --cn icinga2-client1.localdomain
Querying the [Icinga 2 API](12-icinga2-api.md#icinga2-api) on the master requires an [ApiUser](12-icinga2-api.md#icinga2-api-authentication)
object with at least the `actions/generate-ticket`.
object with at least the `actions/generate-ticket` permission.
[root@icinga2-master1.localdomain /]# vim /etc/icinga2/conf.d/api-users.conf
@ -303,6 +318,55 @@ Example: Retrieve the ticket on the Puppet master node and send the compiled cat
to the authorized Puppet agent node which will invoke the
[automated setup steps](06-distributed-monitoring.md#distributed-monitoring-automation-cli-node-setup).
### On-Demand CSR Signing <a id="distributed-monitoring-setup-on-demand-csr-signing"></a>
Icinga 2 v2.8 adds the possibility to sign certificates from clients without
requiring a client ticket for auto-signing.
Instead, the client sends a certificate signing request to specified parent node.
This could either be directly the master, or a satellite which forwards the request
to the signing master.
Advantages:
* Central certificate request signing management.
* No pre-generated ticket is required for client setups.
Disadvantages:
* Asynchronous step for automated deployments.
* Needs client verification on the master.
You can list certificate requests by using the `ca list` CLI command. This also shows
which requests already have been signed.
```
[root@icinga2-master1.localdomain /]# icinga2 ca list
Fingerprint | Timestamp | Signed | Subject
-----------------------------------------------------------------|---------------------|--------|--------
403da5b228df384f07f980f45ba50202529cded7c8182abf96740660caa09727 | 2017/09/06 17:02:40 | * | CN = icinga2-client1.localdomain
71700c28445109416dd7102038962ac3fd421fbb349a6e7303b6033ec1772850 | 2017/09/06 17:20:02 | | CN = icinga2-client2.localdomain
```
**Tip**: Add `--json` to the CLI command to retrieve the details in JSON format.
If you want to sign a specific request, you need to use the `ca sign` CLI command
and pass its fingerprint as argument.
```
[root@icinga2-master1.localdomain /]# icinga2 ca sign 71700c28445109416dd7102038962ac3fd421fbb349a6e7303b6033ec1772850
information/cli: Signed certificate for 'CN = icinga2-client2.localdomain'.
```
## Client/Satellite Setup <a id="distributed-monitoring-setup-satellite-client"></a>
This section describes the setup of a satellite and/or client connected to an
existing master node setup. If you haven't done so already, please [run the master setup](06-distributed-monitoring.md#distributed-monitoring-setup-master).
Icinga 2 on the master node must be running and accepting connections on port `5665`.
### Client/Satellite Linux Setup <a id="distributed-monitoring-setup-client-linux"></a>
Please ensure that you've run all the steps mentioned in the [client/satellite section](06-distributed-monitoring.md#distributed-monitoring-setup-satellite-client).
@ -311,20 +375,166 @@ Install the [Icinga 2 package](02-getting-started.md#setting-up-icinga2) and set
the required [plugins](02-getting-started.md#setting-up-check-plugins) if you haven't done
so already.
The next step is to run the `node wizard` CLI command. Prior to that
ensure to collect the required information:
The next step is to run the `node wizard` CLI command.
In this example we're generating a ticket on the master node `icinga2-master1.localdomain` for the client `icinga2-client1.localdomain`:
[root@icinga2-master1.localdomain /]# icinga2 pki ticket --cn icinga2-client1.localdomain
4f75d2ecd253575fe9180938ebff7cbca262f96e
Note: You don't need this step if you have chosen to use [On-Demand CSR Signing](06-distributed-monitoring.md#distributed-monitoring-setup-on-demand-csr-signing).
Start the wizard on the client `icinga2-client1.localdomain`:
```
[root@icinga2-client1.localdomain /]# icinga2 node wizard
Welcome to the Icinga 2 Setup Wizard!
We will guide you through all required configuration details.
```
Press `Enter` or add `y` to start a satellite or client setup.
```
Please specify if this is a satellite/client setup ('n' installs a master setup) [Y/n]:
```
Press `Enter` to use the proposed name in brackets, or add a specific common name (CN). By convention
this should be the FQDN.
```
Starting the Client/Satellite setup routine...
Please specify the common name (CN) [icinga2-client1.localdomain]: icinga2-client1.localdomain
```
Specify the direct parent for this node. This could be your primary master `icinga2-master1.localdomain`
or a satellite node in a multi level cluster scenario.
```
Please specify the parent endpoint(s) (master or satellite) where this node should connect to:
Master/Satellite Common Name (CN from your master/satellite node): icinga2-master1.localdomain
```
Press `Enter` or choose `y` to establish a connection to the parent node.
```
Do you want to establish a connection to the parent node from this node? [Y/n]:
```
> **Note:**
>
> If this node cannot connect to the parent node, choose `n`. The setup
> wizard will provide instructions for this scenario -- signing questions are disabled then.
Add the connection details for `icinga2-master1.localdomain`.
```
Please specify the master/satellite connection information:
Master/Satellite endpoint host (IP address or FQDN): 192.168.56.101
Master/Satellite endpoint port [5665]: 5665
```
You can add more parent nodes if necessary. Press `Enter` or choose `n`
if you don't want to add any. This comes in handy if you have more than one
parent node, e.g. two masters or two satellites.
```
Add more master/satellite endpoints? [y/N]:
```
Verify the parent node's certificate:
```
Parent certificate information:
Subject: CN = icinga2-master1.localdomain
Issuer: CN = Icinga CA
Valid From: Sep 7 13:41:24 2017 GMT
Valid Until: Sep 3 13:41:24 2032 GMT
Fingerprint: AC 99 8B 2B 3D B0 01 00 E5 21 FA 05 2E EC D5 A9 EF 9E AA E3
Is this information correct? [y/N]: y
```
The setup wizard fetches the parent node's certificate and ask
you to verify this information. This is to prevent MITM attacks or
any kind of untrusted parent relationship.
Note: The certificate is not fetched if you have chosen not to connect
to the parent node.
Proceed with adding the optional client ticket for [CSR auto-signing](06-distributed-monitoring.md#distributed-monitoring-setup-csr-auto-signing):
```
Please specify the request ticket generated on your Icinga 2 master (optional).
(Hint: # icinga2 pki ticket --cn 'icinga2-client1.localdomain'):
4f75d2ecd253575fe9180938ebff7cbca262f96e
```
In case you've chosen to use [On-Demand CSR Signing](06-distributed-monitoring.md#distributed-monitoring-setup-on-demand-csr-signing)
you can leave the ticket question blank.
Instead, Icinga 2 tells you to approve the request later on the master node.
```
No ticket was specified. Please approve the certificate signing request manually
on the master (see 'icinga2 ca list' and 'icinga2 ca sign --help' for details).
```
You can optionally specify a different bind host and/or port.
```
Please specify the API bind host/port (optional):
Bind Host []:
Bind Port []:
```
The next step asks you to accept configuration (required for [config sync mode](06-distributed-monitoring.md#distributed-monitoring-top-down-config-sync))
and commands (required for [command endpoint mode](06-distributed-monitoring.md#distributed-monitoring-top-down-command-endpoint)).
```
Accept config from parent node? [y/N]: y
Accept commands from parent node? [y/N]: y
```
The wizard proceeds and you are good to go.
```
Reconfiguring Icinga...
Done.
Now restart your Icinga 2 daemon to finish the installation!
```
> **Note**
>
> If you have chosen not to connect to the parent node, you cannot start
> Icinga 2 yet. The wizard asked you to manually copy the master's public
> CA certificate file into `/var/lib/icinga2/certs/ca.crt`.
>
> You need to manually sign the CSR on the master node.
Restart Icinga 2 as requested.
```
[root@icinga2-client1.localdomain /]# systemctl restart icinga2
```
Here is an overview of all parameters in detail:
Parameter | Description
--------------------|--------------------
Common name (CN) | **Required.** By convention this should be the host's FQDN. Defaults to the FQDN.
Master common name | **Required.** Use the common name you've specified for your master node before.
Establish connection to the master | **Optional.** Whether the client should attempt to connect to the master or not. Defaults to `y`.
Master endpoint host | **Required if the the client needs to connect to the master.** The master's IP address or FQDN. This information is included in the `Endpoint` object configuration in the `zones.conf` file.
Master endpoint port | **Optional if the the client needs to connect to the master.** The master's listening port. This information is included in the `Endpoint` object configuration.
Add more master endpoints | **Optional.** If you have multiple master nodes configured, add them here.
Master connection for CSR auto-signing | **Required.** The master node's IP address or FQDN and port where the client should request a certificate from. Defaults to the master endpoint host.
Certificate information | **Required.** Verify that the connecting host really is the requested master node.
Request ticket | **Required.** Paste the previously generated [ticket number](06-distributed-monitoring.md#distributed-monitoring-setup-csr-auto-signing).
Establish connection to the parent node | **Optional.** Whether the node should attempt to connect to the parent node or not. Defaults to `y`.
Master/Satellite endpoint host | **Required if the the client needs to connect to the master/satellite.** The parent endpoint's IP address or FQDN. This information is included in the `Endpoint` object configuration in the `zones.conf` file.
Master/Satellite endpoint port | **Optional if the the client needs to connect to the master/satellite.** The parent endpoints's listening port. This information is included in the `Endpoint` object configuration.
Add more master/satellite endpoints | **Optional.** If you have multiple master/satellite nodes configured, add them here.
Parent Certificate information | **Required.** Verify that the connecting host really is the requested master node.
Request ticket | **Optional.** Add the [ticket](06-distributed-monitoring.md#distributed-monitoring-setup-csr-auto-signing) generated on the master.
API bind host | **Optional.** Allows to specify the address the ApiListener is bound to. For advanced usage only.
API bind port | **Optional.** Allows to specify the port the ApiListener is bound to. For advanced usage only (requires changing the default port 5665 everywhere).
Accept config | **Optional.** Whether this node accepts configuration sync from the master node (required for [config sync mode](06-distributed-monitoring.md#distributed-monitoring-top-down-config-sync)). For [security reasons](06-distributed-monitoring.md#distributed-monitoring-security) this defaults to `n`.
@ -334,84 +544,28 @@ The setup wizard will ensure that the following steps are taken:
* Enable the `api` feature.
* Create a certificate signing request (CSR) for the local node.
* Request a signed certificate with the provided ticket number on the master node.
* Allow to verify the master's certificate.
* Store the signed client certificate and ca.crt in `/etc/icinga2/pki`.
* Request a signed certificate i(optional with the provided ticket number) on the master node.
* Allow to verify the parent node's certificate.
* Store the signed client certificate and ca.crt in `/var/lib/icinga2/certs`.
* Update the `zones.conf` file with the new zone hierarchy.
* Update `/etc/icinga2/features-enabled/api.conf` (`accept_config`, `accept_commands`) and `constants.conf`.
In this example we're generating a ticket on the master node `icinga2-master1.localdomain` for the client `icinga2-client1.localdomain`:
[root@icinga2-master1.localdomain /]# icinga2 pki ticket --cn icinga2-client1.localdomain
4f75d2ecd253575fe9180938ebff7cbca262f96e
You can verify that the certificate files are stored in the `/var/lib/icinga2/certs` directory.
The following example shows a client setup for the `icinga2-client1.localdomain` node on CentOS 7. This client
is configured to accept configuration and commands from the master:
[root@icinga2-client1.localdomain /]# icinga2 node wizard
Welcome to the Icinga 2 Setup Wizard!
We'll guide you through all required configuration details.
Please specify if this is a satellite setup ('n' installs a master setup) [Y/n]:
Starting the Node setup routine...
Please specify the common name (CN) [icinga2-client1.localdomain]: icinga2-client1.localdomain
Please specify the master endpoint(s) this node should connect to:
Master Common Name (CN from your master setup): icinga2-master1.localdomain
Do you want to establish a connection to the master from this node? [Y/n]:
Please fill out the master connection information:
Master endpoint host (Your master's IP address or FQDN): 192.168.56.101
Master endpoint port [5665]:
Add more master endpoints? [y/N]:
Please specify the master connection for CSR auto-signing (defaults to master endpoint host):
Host [192.168.56.101]: 192.168.2.101
Port [5665]:
information/base: Writing private key to '/etc/icinga2/pki/icinga2-client1.localdomain.key'.
information/base: Writing X509 certificate to '/etc/icinga2/pki/icinga2-client1.localdomain.crt'.
information/cli: Fetching public certificate from master (192.168.56.101, 5665):
Certificate information:
Subject: CN = icinga2-master1.localdomain
Issuer: CN = Icinga CA
Valid From: Feb 23 14:45:32 2016 GMT
Valid Until: Feb 19 14:45:32 2031 GMT
Fingerprint: AC 99 8B 2B 3D B0 01 00 E5 21 FA 05 2E EC D5 A9 EF 9E AA E3
Is this information correct? [y/N]: y
information/cli: Received trusted master certificate.
Please specify the request ticket generated on your Icinga 2 master.
(Hint: # icinga2 pki ticket --cn 'icinga2-client1.localdomain'): 4f75d2ecd253575fe9180938ebff7cbca262f96e
information/cli: Requesting certificate with ticket '4f75d2ecd253575fe9180938ebff7cbca262f96e'.
information/cli: Created backup file '/etc/icinga2/pki/icinga2-client1.localdomain.crt.orig'.
information/cli: Writing signed certificate to file '/etc/icinga2/pki/icinga2-client1.localdomain.crt'.
information/cli: Writing CA certificate to file '/etc/icinga2/pki/ca.crt'.
Please specify the API bind host/port (optional):
Bind Host []:
Bind Port []:
Accept config from master? [y/N]: y
Accept commands from master? [y/N]: y
information/cli: Disabling the Notification feature.
Disabling feature notification. Make sure to restart Icinga 2 for these changes to take effect.
information/cli: Enabling the Apilistener feature.
information/cli: Generating local zones.conf.
information/cli: Dumping config items to file '/etc/icinga2/zones.conf'.
information/cli: Updating constants.conf.
information/cli: Updating constants file '/etc/icinga2/constants.conf'.
information/cli: Updating constants file '/etc/icinga2/constants.conf'.
Done.
Now restart your Icinga 2 daemon to finish the installation!
[root@icinga2-client1.localdomain /]# systemctl restart icinga2
As you can see, the certificate files are stored in the `/etc/icinga2/pki` directory.
> **Note**
>
> If the client is not directly connected to the certificate signing master,
> signing requests and responses might need some minutes to fully update the client certificates.
>
> If you have chosen to use [On-Demand CSR Signing](06-distributed-monitoring.md#distributed-monitoring-setup-on-demand-csr-signing)
> certificates need to be signed on the master first.
Now that you've successfully installed a satellite/client, please proceed to
the [configuration modes](06-distributed-monitoring.md#distributed-monitoring-configuration-modes).
### Client/Satellite Windows Setup <a id="distributed-monitoring-setup-client-windows"></a>
Download the MSI-Installer package from [https://packages.icinga.com/windows/](https://packages.icinga.com/windows/).
@ -2276,6 +2430,13 @@ Open Icinga Web 2 and check your newly added Windows NSClient++ check :)
You can find additional hints in this section if you prefer to go your own route
with automating setups (setup, certificates, configuration).
### Certificate Auto-Renewal <a id="distributed-monitoring-certificate-auto-renewal"></a>
Icinga 2 v2.8+ adds the possibility that nodes request certificate updates
on their own. If their expiration date is soon enough, they automatically
renew their already signed certificate by sending a signing request to the
parent node.
### High-Availability for Icinga 2 Features <a id="distributed-monitoring-high-availability-features"></a>
All nodes in the same zone require that you enable the same features for high-availability (HA).
@ -2469,24 +2630,24 @@ Sign the CSR with the previously created CA:
[root@icinga2-master1.localdomain /root]# icinga2 pki sign-csr --csr icinga2-master1.localdomain.csr --cert icinga2-master1.localdomain
Copy the host's certificate files and the public CA certificate to `/etc/icinga2/pki`:
Copy the host's certificate files and the public CA certificate to `/var/lib/icinga2/certs`:
[root@icinga2-master1.localdomain /root]# mkdir -p /etc/icinga2/pki
[root@icinga2-master1.localdomain /root]# cp icinga2-master1.localdomain.{crt,key} /etc/icinga2/pki
[root@icinga2-master1.localdomain /root]# cp /var/lib/icinga2/ca/ca.crt /etc/icinga2/pki
[root@icinga2-master1.localdomain /root]# mkdir -p /var/lib/icinga2/certs
[root@icinga2-master1.localdomain /root]# cp icinga2-master1.localdomain.{crt,key} /var/lib/icinga2/certs
[root@icinga2-master1.localdomain /root]# cp /var/lib/icinga2/ca/ca.crt /var/lib/icinga2/certs
Ensure that proper permissions are set (replace `icinga` with the Icinga 2 daemon user):
[root@icinga2-master1.localdomain /root]# chown -R icinga:icinga /etc/icinga2/pki
[root@icinga2-master1.localdomain /root]# chmod 600 /etc/icinga2/pki/*.key
[root@icinga2-master1.localdomain /root]# chmod 644 /etc/icinga2/pki/*.crt
[root@icinga2-master1.localdomain /root]# chown -R icinga:icinga /var/lib/icinga2/certs
[root@icinga2-master1.localdomain /root]# chmod 600 /var/lib/icinga2/certs/*.key
[root@icinga2-master1.localdomain /root]# chmod 644 /var/lib/icinga2/certs/*.crt
The CA public and private key are stored in the `/var/lib/icinga2/ca` directory. Keep this path secure and include
it in your backups.
Example for creating multiple certificates at once:
[root@icinga2-master1.localdomain /etc/icinga2/pki]# for node in icinga2-master1.localdomain icinga2-master2.localdomain icinga2-satellite1.localdomain; do icinga2 pki new-cert --cn $node --csr $node.csr --key $node.key; done
[root@icinga2-master1.localdomain /var/lib/icinga2/certs]# for node in icinga2-master1.localdomain icinga2-master2.localdomain icinga2-satellite1.localdomain; do icinga2 pki new-cert --cn $node --csr $node.csr --key $node.key; done
information/base: Writing private key to 'icinga2-master1.localdomain.key'.
information/base: Writing certificate signing request to 'icinga2-master1.localdomain.csr'.
information/base: Writing private key to 'icinga2-master2.localdomain.key'.
@ -2494,7 +2655,7 @@ Example for creating multiple certificates at once:
information/base: Writing private key to 'icinga2-satellite1.localdomain.key'.
information/base: Writing certificate signing request to 'icinga2-satellite1.localdomain.csr'.
[root@icinga2-master1.localdomain /etc/icinga2/pki]# for node in icinga2-master1.localdomain icinga2-master2.localdomain icinga2-satellite1.localdomain; do sudo icinga2 pki sign-csr --csr $node.csr --cert $node.crt; done
[root@icinga2-master1.localdomain /var/lib/icinga2/certs]# for node in icinga2-master1.localdomain icinga2-master2.localdomain icinga2-satellite1.localdomain; do sudo icinga2 pki sign-csr --csr $node.csr --cert $node.crt; done
information/pki: Writing certificate to file 'icinga2-master1.localdomain.crt'.
information/pki: Writing certificate to file 'icinga2-master2.localdomain.crt'.
information/pki: Writing certificate to file 'icinga2-satellite1.localdomain.crt'.
@ -2555,11 +2716,11 @@ host/port you can specify it like this:
#### Node Setup with Satellites/Clients <a id="distributed-monitoring-automation-cli-node-setup-satellite-client"></a>
Make sure that the `/etc/icinga2/pki` exists and is owned by the `icinga`
Make sure that the `/var/lib/icinga2/certs` exists and is owned by the `icinga`
user (or the user Icinga 2 is running as).
[root@icinga2-client1.localdomain /]# mkdir -p /etc/icinga2/pki
[root@icinga2-client1.localdomain /]# chown -R icinga:icinga /etc/icinga2/pki
[root@icinga2-client1.localdomain /]# mkdir -p /var/lib/icinga2/certs
[root@icinga2-client1.localdomain /]# chown -R icinga:icinga /var/lib/icinga2/certs
First you'll need to generate a new local self-signed certificate.
Pass the following details to the `pki new-cert` CLI command:
@ -2567,13 +2728,13 @@ Pass the following details to the `pki new-cert` CLI command:
Parameter | Description
--------------------|--------------------
Common name (CN) | **Required.** By convention this should be the host's FQDN. Defaults to the FQDN.
Client certificate files | **Required.** These generated files will be put into the specified location (--key and --file). By convention this should be using `/etc/icinga2/pki` as directory.
Client certificate files | **Required.** These generated files will be put into the specified location (--key and --file). By convention this should be using `/var/lib/icinga2/certs` as directory.
Example:
[root@icinga2-client1.localdomain /]# icinga2 pki new-cert --cn icinga2-client1.localdomain \
--key /etc/icinga2/pki/icinga2-client1.localdomain.key \
--cert /etc/icinga2/pki/icinga2-client1.localdomain.crt
--key /var/lib/icinga2/certs/icinga2-client1.localdomain.key \
--cert /var/lib/icinga2/certs/icinga2-client1.localdomain.crt
Request the master certificate from the master host (`icinga2-master1.localdomain`)
and store it as `trusted-master.crt`. Review it and continue.
@ -2588,9 +2749,9 @@ Pass the following details to the `pki save-cert` CLI command:
Example:
[root@icinga2-client1.localdomain /]# icinga2 pki save-cert --key /etc/icinga2/pki/icinga2-client1.localdomain.key \
--cert /etc/icinga2/pki/icinga2-client1.localdomain.crt \
--trustedcert /etc/icinga2/pki/trusted-master.crt \
[root@icinga2-client1.localdomain /]# icinga2 pki save-cert --key /var/lib/icinga2/certs/icinga2-client1.localdomain.key \
--cert /var/lib/icinga2/certs/icinga2-client1.localdomain.crt \
--trustedcert /var/lib/icinga2/certs/trusted-master.crt \
--host icinga2-master1.localdomain
Continue with the additional node setup step. Specify a local endpoint and zone name (`icinga2-client1.localdomain`)
@ -2617,7 +2778,7 @@ Example:
--endpoint icinga2-master1.localdomain \
--zone icinga2-client1.localdomain \
--master_host icinga2-master1.localdomain \
--trustedcert /etc/icinga2/pki/trusted-master.crt \
--trustedcert /var/lib/icinga2/certs/trusted-master.crt \
--accept-commands --accept-config
In case the client should connect to the master node, you'll

View File

@ -181,7 +181,7 @@ SNMP Traps can be received and filtered by using [SNMPTT](http://snmptt.sourcefo
and specific trap handlers passing the check results to Icinga 2.
Following the SNMPTT [Format](http://snmptt.sourceforge.net/docs/snmptt.shtml#SNMPTT.CONF-FORMAT)
documentation and the Icinga external command syntax found [here](23-appendix.md#external-commands-list-detail)
documentation and the Icinga external command syntax found [here](24-appendix.md#external-commands-list-detail)
we can create generic services that can accommodate any number of hosts for a given scenario.
### Simple SNMP Traps <a id="simple-traps"></a>

View File

@ -31,12 +31,15 @@ The `NodeName` constant must be defined in [constants.conf](04-configuring-icing
Example:
object ApiListener "api" {
cert_path = SysconfDir + "/icinga2/pki/" + NodeName + ".crt"
key_path = SysconfDir + "/icinga2/pki/" + NodeName + ".key"
ca_path = SysconfDir + "/icinga2/pki/ca.crt"
}
```
object ApiListener "api" {
cert_path = LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".crt"
key_path = LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".key"
ca_path = LocalStateDir + "/lib/icinga2/certs/ca.crt"
ticket_salt = TicketSalt
}
```
Configuration Attributes:
@ -45,6 +48,7 @@ Configuration Attributes:
cert\_path |**Required.** Path to the public key.
key\_path |**Required.** Path to the private key.
ca\_path |**Required.** Path to the CA certificate file.
ticket\_salt |**Optional.** Private key for auto-signing. **Required** for a signing master instance.
crl\_path |**Optional.** Path to the CRL file.
bind\_host |**Optional.** The IP address the api listener should be bound to. Defaults to `0.0.0.0`.
bind\_port |**Optional.** The port the api listener should be bound to. Defaults to `5665`.

View File

@ -78,11 +78,16 @@ Example for PostgreSQL:
(1 Zeile)
A detailed list on the available table attributes can be found in the [DB IDO Schema documentation](23-appendix.md#schema-db-ido).
A detailed list on the available table attributes can be found in the [DB IDO Schema documentation](24-appendix.md#schema-db-ido).
## External Commands <a id="external-commands"></a>
> **Note**
>
> Please use the [REST API](12-icinga2-api.md#icinga2-api) as modern and secure alternative
> for external actions.
Icinga 2 provides an external command pipe for processing commands
triggering specific actions (for example rescheduling a service check
through the web interface).
@ -106,7 +111,7 @@ a forced service check:
Oct 17 15:01:25 icinga-server icinga2: Executing external command: [1382014885] SCHEDULE_FORCED_SVC_CHECK;localhost;ping4;1382014885
Oct 17 15:01:25 icinga-server icinga2: Rescheduling next check for service 'ping4'
A list of currently supported external commands can be found [here](23-appendix.md#external-commands-list-detail).
A list of currently supported external commands can be found [here](24-appendix.md#external-commands-list-detail).
Detailed information on the commands and their required parameters can be found
on the [Icinga 1.x documentation](https://docs.icinga.com/latest/en/extcommands2.html).
@ -441,7 +446,7 @@ re-implementation of the Livestatus protocol which is compatible with MK
Livestatus.
Details on the available tables and attributes with Icinga 2 can be found
in the [Livestatus Schema](23-appendix.md#schema-livestatus) section.
in the [Livestatus Schema](24-appendix.md#schema-livestatus) section.
You can enable Livestatus using icinga2 feature enable:
@ -517,7 +522,7 @@ Example using the tcp socket listening on port `6558`:
### Livestatus COMMAND Queries <a id="livestatus-command-queries"></a>
A list of available external commands and their parameters can be found [here](23-appendix.md#external-commands-list-detail)
A list of available external commands and their parameters can be found [here](24-appendix.md#external-commands-list-detail)
$ echo -e 'COMMAND <externalcommandstring>' | netcat 127.0.0.1 6558
@ -618,7 +623,7 @@ Default separators.
The `commands` table is populated with `CheckCommand`, `EventCommand` and `NotificationCommand` objects.
A detailed list on the available table attributes can be found in the [Livestatus Schema documentation](23-appendix.md#schema-livestatus).
A detailed list on the available table attributes can be found in the [Livestatus Schema documentation](24-appendix.md#schema-livestatus).
## Status Data Files <a id="status-data"></a>

View File

@ -29,7 +29,7 @@ findings and details please.
* The newest Icinga 2 crash log if relevant, located in `/var/log/icinga2/crash`
* Additional details
* If the check command failed, what's the output of your manual plugin tests?
* In case of [debugging](20-development.md#development) Icinga 2, the full back traces and outputs
* In case of [debugging](21-development.md#development) Icinga 2, the full back traces and outputs
## Analyze your Environment <a id="troubleshooting-analyze-environment"></a>
@ -666,9 +666,9 @@ the following
Steps on the client `icinga2-node2.localdomain`:
# ls -la /etc/icinga2/pki
# ls -la /var/lib/icinga2/certs
# cd /etc/icinga2/pki/
# cd /var/lib/icinga2/certs/
# openssl x509 -in icinga2-node2.localdomain.crt -text
Certificate:
Data:
@ -688,7 +688,7 @@ Steps on the client `icinga2-node2.localdomain`:
Try to manually connect from `icinga2-node2.localdomain` to the master node `icinga2-node1.localdomain`:
# openssl s_client -CAfile /etc/icinga2/pki/ca.crt -cert /etc/icinga2/pki/icinga2-node2.localdomain.crt -key /etc/icinga2/pki/icinga2-node2.localdomain.key -connect icinga2-node1.localdomain:5665
# openssl s_client -CAfile /var/lib/icinga2/certs/ca.crt -cert /var/lib/icinga2/certs/icinga2-node2.localdomain.crt -key /var/lib/icinga2/certs/icinga2-node2.localdomain.key -connect icinga2-node1.localdomain:5665
CONNECTED(00000003)
---
@ -712,19 +712,19 @@ If these messages do not go away, make sure to [verify the master and client cer
#### Cluster Troubleshooting SSL Certificate Verification <a id="troubleshooting-cluster-ssl-certificate-verification"></a>
Make sure to verify the client's certificate and its received `ca.crt` in `/etc/icinga2/pki` and ensure that
Make sure to verify the client's certificate and its received `ca.crt` in `/var/lib/icinga2/certs` and ensure that
both instances are signed by the **same CA**.
# openssl verify -verbose -CAfile /etc/icinga2/pki/ca.crt /etc/icinga2/pki/icinga2-node1.localdomain.crt
# openssl verify -verbose -CAfile /var/lib/icinga2/certs/ca.crt /var/lib/icinga2/certs/icinga2-node1.localdomain.crt
icinga2-node1.localdomain.crt: OK
# openssl verify -verbose -CAfile /etc/icinga2/pki/ca.crt /etc/icinga2/pki/icinga2-node2.localdomain.crt
# openssl verify -verbose -CAfile /var/lib/icinga2/certs/ca.crt /var/lib/icinga2/certs/icinga2-node2.localdomain.crt
icinga2-node2.localdomain.crt: OK
Fetch the `ca.crt` file from the client node and compare it to your master's `ca.crt` file:
# scp icinga2-node2:/etc/icinga2/pki/ca.crt test-client-ca.crt
# diff -ur /etc/icinga2/pki/ca.crt test-client-ca.crt
# scp icinga2-node2:/var/lib/icinga2/certs/ca.crt test-client-ca.crt
# diff -ur /var/lib/icinga2/certs/ca.crt test-client-ca.crt
On SLES11 you'll need to use the `openssl1` command instead of `openssl`.

View File

@ -1,8 +1,20 @@
# Upgrading Icinga 2 <a id="upgrading-icinga-2"></a>
# Upgrading Icinga 2 <a id="upgrading-icinga-2"></a>
Upgrading Icinga 2 is usually quite straightforward. Ordinarily the only manual steps involved
are scheme updates for the IDO database.
## Upgrading to v2.8 <a id="upgrading-to-2-8"></a>
The default certificate path was changed from `/etc/icinga2/pki` to
`/var/lib/icinga2/certs`.
This applies to Windows clients in the same way: `%ProgramData%\etc\icinga2\pki`
was moved to `%ProgramData%`\var\lib\icinga2\certs`.
The [setup CLI commands](06-distributed-monitoring.md#distributed-monitoring-setup-master) and the
default [ApiListener configuration](06-distributed-monitoring.md#distributed-monitoring-apilistener)
have been adjusted to these paths too.
## Upgrading the MySQL database <a id="upgrading-mysql-db"></a>
If you're upgrading an existing Icinga 2 instance, you should check the

View File

@ -0,0 +1,270 @@
# Technical Concepts <a id="technical-concepts"></a>
This chapter provides insights into specific Icinga 2
components, libraries, features and any other technical concept
and design.
<!--
## Application <a id="technical-concepts-application"></a>
### Libraries <a id="technical-concepts-application-libraries"></a>
## Configuration <a id="technical-concepts-configuration"></a>
### Compiler <a id="technical-concepts-configuration-compiler"></a>
-->
## Features <a id="technical-concepts-features"></a>
Features are implemented in specific libraries and can be enabled
using CLI commands.
Features either write specific data or receive data.
Examples for writing data: [DB IDO](14-features.md#db-ido), [Graphite](14-features.md#graphite-carbon-cache-writer), [InfluxDB](14-features.md#influxdb-writer). [GELF](14-features.md#gelfwriter), etc.
Examples for receiving data: [REST API](12-icinga2-api.md#icinga2-api), etc.
The implementation of features makes use of existing libraries
and functionality. This makes the code more abstract, but shorter
and easier to read.
Features register callback functions on specific events they want
to handle. For example the `GraphiteWriter` feature subscribes to
new CheckResult events.
Each time Icinga 2 receives and processes a new check result, this
event is triggered and forwarded to all subscribers.
The GraphiteWriter feature calls the registered function and processes
the received data. Features which connect Icinga 2 to external interfaces
normally parse and reformat the received data into an applicable format.
The GraphiteWriter uses a TCP socket to communicate with the carbon cache
daemon of Graphite. The InfluxDBWriter is instead writing bulk metric messages
to InfluxDB's HTTP API.
## Cluster <a id="technical-concepts-cluster"></a>
### Communication <a id="technical-concepts-cluster-communication"></a>
Icinga 2 uses its own certificate authority (CA) by default. The
public and private CA keys can be generated on the signing master.
Each node certificate must be signed by the private CA key.
Note: The following description uses `parent node` and `child node`.
This also applies to nodes in the same cluster zone.
During the connection attempt, an SSL handshake is performed.
If the public certificate of a child node is not signed by the same
CA, the child node is not trusted and the connection will be closed.
If the SSL handshake succeeds, the parent node reads the
certificate's common name (CN) of the child node and looks for
a local Endpoint object name configuration.
If there is no Endpoint object found, further communication
(runtime and config sync, etc.) is terminated.
The child node also checks the CN from the parent node's public
certificate. If the child node does not find any local Endpoint
object name configuration, it will not trust the parent node.
Both checks prevent accepting cluster messages from an untrusted
source endpoint.
If an Endpoint match was found, there is one additional security
mechanism in place: Endpoints belong to a Zone hierarchy.
Several cluster messages can only be sent "top down", others like
check results are allowed being sent from the child to the parent node.
Once this check succeeds the cluster messages are exchanged and processed.
### CSR Signing <a id="technical-concepts-cluster-csr-signing"></a>
In order to make things easier, Icinga 2 provides built-in methods
to allow child nodes to request a signed certificate from the
signing master.
Icinga 2 v2.8 introduces the possibility to request certificates
from indirectly connected nodes. This is required for multi level
cluster environments with masters, satellites and clients.
CSR Signing in general starts with the master setup. This step
ensures that the master is in a working CSR signing state with:
* public and private CA key in `/var/lib/icinga2/ca`
* private `TicketSalt` constant defined inside the `api` feature
* Cluster communication is ready and Icinga 2 listens on port 5665
The child node setup which is run with CLI commands will now
attempt to connect to the parent node. This is not necessarily
the signing master instance, but could also be a parent satellite node.
During this process the child node asks the user to verify the
parent node's public certificate to prevent MITM attacks.
There are two methods to request signed certificates:
* Add the ticket into the request. This ticket was generated on the master
beforehand and contains hashed details for which client it has been created.
The signing master uses this information to automatically sign the certificate
request.
* Do not add a ticket into the request. It will be sent to the signing master
which stores the pending request. Manual user interaction with CLI commands
is necessary to sign the request.
The certificate request is sent as `pki::RequestCertificate` cluster
message to the parent node.
If the parent node is not the signing master, it stores the request
in `/var/lib/icinga2/certificate-requests` and forwards the
cluster message to its parent node.
Once the message arrives on the signing master, it first verifies that
the sent certificate request is valid. This is to prevent unwanted errors
or modified requests from the "proxy" node.
After verification, the signing master checks if the request contains
a valid signing ticket. It hashes the certificate's common name and
compares the value to the received ticket number.
If the ticket is valid, the certificate request is immediately signed
with CA key. The request is sent back to the client inside a `pki::UpdateCertificate`
cluster message.
If the child node was not the certificate request origin, it only updates
the cached request for the child node and send another cluster message
down to its child node (e.g. from a satellite to a client).
If no ticket was specified, the signing master waits until the
`ca sign` CLI command manually signed the certificate.
> **Note**
>
> Push notifications for manual request signing is not yet implemented (TODO).
Once the child node reconnects it synchronizes all signed certificate requests.
This takes some minutes and requires all nodes to reconnect to each other.
#### CSR Signing: Clients without parent connection <a id="technical-concepts-cluster-csr-signing-clients-no-connection"></a>
There is an additional scenario: The setup on a child node does
not necessarily need a connection to the parent node.
This mode leaves the node in a semi-configured state. You need
to manually copy the master's public CA key into `/var/lib/icinga2/certs/ca.crt`
on the client before starting Icinga 2.
The parent node needs to actively connect to the child node.
Once this connections succeeds, the child node will actively
request a signed certificate.
The update procedure works the same way as above.
### High Availability <a id="technical-concepts-cluster-ha"></a>
High availability is automatically enabled between two nodes in the same
cluster zone.
This requires the same configuration and enabled features on both nodes.
HA zone members trust each other and share event updates as cluster messages.
This includes for example check results, next check timestamp updates, acknowledgements
or notifications.
This ensures that both nodes are synchronized. If one node goes away, the
remaining node takes over and continues as normal.
Cluster nodes automatically determine the authority for configuration
objects. This results in activated but paused objects. You can verify
that by querying the `paused` attribute for all objects via REST API
or debug console.
Nodes inside a HA zone calculate the object authority independent from each other.
The number of endpoints in a zone is defined through the configuration. This number
is used inside a local modulo calculation to determine whether the node feels
responsible for this object or not.
This object authority is important for selected features explained below.
Since features are configuration objects too, you must ensure that all nodes
inside the HA zone share the same enabled features. If configured otherwise,
one might have a checker feature on the left node, nothing on the right node.
This leads to late check results because one half is not executed by the right
node which holds half of the object authorities.
### High Availability: Checker <a id="technical-concepts-cluster-ha-checker"></a>
The `checker` feature only executes checks for `Checkable` objects (Host, Service)
where it is authoritative.
That way each node only executes checks for a segment of the overall configuration objects.
The cluster message routing ensures that all check results are synchronized
to nodes which are not authoritative for this configuration object.
### High Availability: Notifications <a id="technical-concepts-cluster-notifications"></a>
The `notification` feature only sends notifications for `Notification` objects
where it is authoritative.
That way each node only executes notifications for a segment of all notification objects.
Notified users and other event details are synchronized throughout the cluster.
This is required if for example the DB IDO feature is active on the other node.
### High Availability: DB IDO <a id="technical-concepts-cluster-ha-ido"></a>
If you don't have HA enabled for the IDO feature, both nodes will
write their status and historical data to their own separate database
backends.
In order to avoid data separation and a split view (each node would require its
own Icinga Web 2 installation on top), the high availability option was added
to the DB IDO feature. This is enabled by default with the `enable_ha` setting.
This requires a central database backend. Best practice is to use a MySQL cluster
with a virtual IP.
Both Icinga 2 nodes require the connection and credential details configured in
their DB IDO feature.
During startup Icinga 2 calculates whether the feature configuration object
is authoritative on this node or not. The order is an alpha-numeric
comparison, e.g. if you have `master1` and `master2`, Icinga 2 will enable
the DB IDO feature on `master2` by default.
If the connection between endpoints drops, the object authority is re-calculated.
In order to prevent data duplication in a split-brain scenario where both
nodes would write into the same database, there is another safety mechanism
in place.
The split-brain decision which node will write to the database is calculated
from a quorum inside the `programstatus` table. Each node
verifies whether the `endpoint_name` column is not itself on database connect.
In addition to that the DB IDO feature compares the `last_update_time` column
against the current timestamp plus the configured `failover_timeout` offset.
That way only one active DB IDO feature writes to the database, even if they
are not currently connected in a cluster zone. This prevents data duplication
in historical tables.
<!--
## REST API <a id="technical-concepts-rest-api"></a>
Icinga 2 provides its own HTTP server which shares the port 5665 with
the JSON-RPC cluster protocol.
-->

View File

@ -11,7 +11,7 @@ on your migration requirements.
For a long-term migration of your configuration you should consider re-creating
your configuration based on the proposed Icinga 2 configuration paradigm.
Please read the [next chapter](22-migrating-from-icinga-1x.md#differences-1x-2) to find out more about the differences
Please read the [next chapter](23-migrating-from-icinga-1x.md#differences-1x-2) to find out more about the differences
between 1.x and 2.
### Manual Config Migration Hints <a id="manual-config-migration-hints"></a>
@ -24,7 +24,7 @@ The examples are taken from Icinga 1.x test and production environments and conv
straight into a possible Icinga 2 format. If you found a different strategy, please
let us know!
If you require in-depth explanations, please check the [next chapter](22-migrating-from-icinga-1x.md#differences-1x-2).
If you require in-depth explanations, please check the [next chapter](23-migrating-from-icinga-1x.md#differences-1x-2).
#### Manual Config Migration Hints for Intervals <a id="manual-config-migration-hints-Intervals"></a>
@ -185,7 +185,7 @@ While you could manually migrate this like (please note the new generic command
#### Manual Config Migration Hints for Runtime Macros <a id="manual-config-migration-hints-runtime-macros"></a>
Runtime macros have been renamed. A detailed comparison table can be found [here](22-migrating-from-icinga-1x.md#differences-1x-2-runtime-macros).
Runtime macros have been renamed. A detailed comparison table can be found [here](23-migrating-from-icinga-1x.md#differences-1x-2-runtime-macros).
For example, accessing the service check output looks like the following in Icinga 1.x:
@ -257,7 +257,7 @@ while the service check command resolves its value to the service attribute attr
#### Manual Config Migration Hints for Contacts (Users) <a id="manual-config-migration-hints-contacts-users"></a>
Contacts in Icinga 1.x act as users in Icinga 2, but do not have any notification commands specified.
This migration part is explained in the [next chapter](22-migrating-from-icinga-1x.md#manual-config-migration-hints-notifications).
This migration part is explained in the [next chapter](23-migrating-from-icinga-1x.md#manual-config-migration-hints-notifications).
define contact{
contact_name testconfig-user
@ -267,7 +267,7 @@ This migration part is explained in the [next chapter](22-migrating-from-icinga-
email icinga@localhost
}
The `service_notification_options` can be [mapped](22-migrating-from-icinga-1x.md#manual-config-migration-hints-notification-filters)
The `service_notification_options` can be [mapped](23-migrating-from-icinga-1x.md#manual-config-migration-hints-notification-filters)
into generic `state` and `type` filters, if additional notification filtering is required. `alias` gets
renamed to `display_name`.
@ -319,7 +319,7 @@ Assign it to the host or service and set the newly generated notification comman
Convert the `notification_options` attribute from Icinga 1.x to Icinga 2 `states` and `types`. Details
[here](22-migrating-from-icinga-1x.md#manual-config-migration-hints-notification-filters). Add the notification period.
[here](23-migrating-from-icinga-1x.md#manual-config-migration-hints-notification-filters). Add the notification period.
states = [ OK, Warning, Critical ]
types = [ Recovery, Problem, Custom ]
@ -556,7 +556,7 @@ enabled.
assign where "hg_svcdep2" in host.groups
}
Host dependencies are explained in the [next chapter](22-migrating-from-icinga-1x.md#manual-config-migration-hints-host-parents).
Host dependencies are explained in the [next chapter](23-migrating-from-icinga-1x.md#manual-config-migration-hints-host-parents).
@ -955,7 +955,7 @@ In Icinga 1.x arguments are specified in the `check_command` attribute and
are separated from the command name using an exclamation mark (`!`).
Please check the migration hints for a detailed
[migration example](22-migrating-from-icinga-1x.md#manual-config-migration-hints-check-command-arguments).
[migration example](23-migrating-from-icinga-1x.md#manual-config-migration-hints-check-command-arguments).
> **Note**
>

View File

@ -692,16 +692,16 @@ Not supported: `debug_info`.
#### Livestatus Hostsbygroup Table Attributes <a id="schema-livestatus-hostsbygroup-table-attributes"></a>
All [hosts](23-appendix.md#schema-livestatus-hosts-table-attributes) table attributes grouped with
the [hostgroups](23-appendix.md#schema-livestatus-hostgroups-table-attributes) table prefixed with `hostgroup_`.
All [hosts](24-appendix.md#schema-livestatus-hosts-table-attributes) table attributes grouped with
the [hostgroups](24-appendix.md#schema-livestatus-hostgroups-table-attributes) table prefixed with `hostgroup_`.
#### Livestatus Servicesbygroup Table Attributes <a id="schema-livestatus-servicesbygroup-table-attributes"></a>
All [services](23-appendix.md#schema-livestatus-services-table-attributes) table attributes grouped with
the [servicegroups](23-appendix.md#schema-livestatus-servicegroups-table-attributes) table prefixed with `servicegroup_`.
All [services](24-appendix.md#schema-livestatus-services-table-attributes) table attributes grouped with
the [servicegroups](24-appendix.md#schema-livestatus-servicegroups-table-attributes) table prefixed with `servicegroup_`.
#### Livestatus Servicesbyhostgroup Table Attributes <a id="schema-livestatus-servicesbyhostgroup-table-attributes"></a>
All [services](23-appendix.md#schema-livestatus-services-table-attributes) table attributes grouped with
the [hostgroups](23-appendix.md#schema-livestatus-hostgroups-table-attributes) table prefixed with `hostgroup_`.
All [services](24-appendix.md#schema-livestatus-services-table-attributes) table attributes grouped with
the [hostgroups](24-appendix.md#schema-livestatus-hostgroups-table-attributes) table prefixed with `hostgroup_`.

View File

@ -3,9 +3,9 @@
*/
object ApiListener "api" {
cert_path = SysconfDir + "/icinga2/pki/" + NodeName + ".crt"
key_path = SysconfDir + "/icinga2/pki/" + NodeName + ".key"
ca_path = SysconfDir + "/icinga2/pki/ca.crt"
cert_path = LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".crt"
key_path = LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".key"
ca_path = LocalStateDir + "/lib/icinga2/certs/ca.crt"
ticket_salt = TicketSalt
}

View File

@ -575,6 +575,12 @@ boost::shared_ptr<X509> CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject)
return CreateCert(pubkey, subject, X509_get_subject_name(cacert.get()), privkey, false);
}
boost::shared_ptr<X509> CreateCertIcingaCA(const boost::shared_ptr<X509>& cert)
{
boost::shared_ptr<EVP_PKEY> pkey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(cert.get()), EVP_PKEY_free);
return CreateCertIcingaCA(pkey.get(), X509_get_subject_name(cert.get()));
}
String CertificateToString(const boost::shared_ptr<X509>& cert)
{
BIO *mem = BIO_new(BIO_s_mem());
@ -590,6 +596,21 @@ String CertificateToString(const boost::shared_ptr<X509>& cert)
return result;
}
boost::shared_ptr<X509> StringToCertificate(const String& cert)
{
BIO *bio = BIO_new(BIO_s_mem());
BIO_write(bio, (const void *)cert.CStr(), cert.GetLength());
X509 *rawCert = PEM_read_bio_X509_AUX(bio, NULL, NULL, NULL);
BIO_free(bio);
if (!rawCert)
BOOST_THROW_EXCEPTION(std::invalid_argument("The specified X509 certificate is invalid."));
return boost::shared_ptr<X509>(rawCert, X509_free);
}
String PBKDF2_SHA1(const String& password, const String& salt, int iterations)
{
unsigned char digest[SHA_DIGEST_LENGTH];
@ -707,4 +728,24 @@ String RandomString(int length)
return result;
}
bool VerifyCertificate(const boost::shared_ptr<X509>& caCertificate, const boost::shared_ptr<X509>& certificate)
{
X509_STORE *store = X509_STORE_new();
if (!store)
return false;
X509_STORE_add_cert(store, caCertificate.get());
X509_STORE_CTX *csc = X509_STORE_CTX_new();
X509_STORE_CTX_init(csc, store, certificate.get(), NULL);
int rc = X509_verify_cert(csc);
X509_STORE_CTX_free(csc);
X509_STORE_free(store);
return rc == 1;
}
}

View File

@ -48,11 +48,14 @@ int I2_BASE_API MakeX509CSR(const String& cn, const String& keyfile, const Strin
boost::shared_ptr<X509> I2_BASE_API CreateCert(EVP_PKEY *pubkey, X509_NAME *subject, X509_NAME *issuer, EVP_PKEY *cakey, bool ca);
String I2_BASE_API GetIcingaCADir(void);
String I2_BASE_API CertificateToString(const boost::shared_ptr<X509>& cert);
boost::shared_ptr<X509> I2_BASE_API StringToCertificate(const String& cert);
boost::shared_ptr<X509> I2_BASE_API CreateCertIcingaCA(EVP_PKEY *pubkey, X509_NAME *subject);
boost::shared_ptr<X509> I2_BASE_API CreateCertIcingaCA(const boost::shared_ptr<X509>& cert);
String I2_BASE_API PBKDF2_SHA1(const String& password, const String& salt, int iterations);
String I2_BASE_API SHA1(const String& s, bool binary = false);
String I2_BASE_API SHA256(const String& s);
String I2_BASE_API RandomString(int length);
bool I2_BASE_API VerifyCertificate(const boost::shared_ptr<X509>& caCertificate, const boost::shared_ptr<X509>& certificate);
class I2_BASE_API openssl_error : virtual public std::exception, virtual public boost::exception { };

View File

@ -17,6 +17,7 @@
set(cli_SOURCES
apisetupcommand.cpp apisetuputility.cpp
calistcommand.cpp casigncommand.cpp
nodeaddcommand.cpp nodeblackandwhitelistcommand.cpp nodelistcommand.cpp noderemovecommand.cpp
nodesetcommand.cpp nodesetupcommand.cpp nodeupdateconfigcommand.cpp nodewizardcommand.cpp nodeutility.cpp
clicommand.cpp
@ -25,7 +26,6 @@ set(cli_SOURCES
featureenablecommand.cpp featuredisablecommand.cpp featurelistcommand.cpp featureutility.cpp
objectlistcommand.cpp objectlistutility.cpp
pkinewcacommand.cpp pkinewcertcommand.cpp pkisigncsrcommand.cpp pkirequestcommand.cpp pkisavecertcommand.cpp pkiticketcommand.cpp
pkiutility.cpp
repositoryclearchangescommand.cpp repositorycommitcommand.cpp repositoryobjectcommand.cpp repositoryutility.cpp
variablegetcommand.cpp variablelistcommand.cpp variableutility.cpp
troubleshootcommand.cpp

View File

@ -18,9 +18,10 @@
******************************************************************************/
#include "cli/apisetuputility.hpp"
#include "cli/pkiutility.hpp"
#include "cli/nodeutility.hpp"
#include "cli/featureutility.hpp"
#include "remote/apilistener.hpp"
#include "remote/pkiutility.hpp"
#include "base/logger.hpp"
#include "base/console.hpp"
#include "base/application.hpp"
@ -68,7 +69,7 @@ bool ApiSetupUtility::SetupMasterCertificates(const String& cn)
if (PkiUtility::NewCa() > 0)
Log(LogWarning, "cli", "Found CA, skipping and using the existing one.");
String pki_path = PkiUtility::GetPkiPath();
String pki_path = ApiListener::GetCertsDir();
Utility::MkDirP(pki_path, 0700);
String user = ScriptGlobal::Get("RunAsUser");
@ -116,7 +117,7 @@ bool ApiSetupUtility::SetupMasterCertificates(const String& cn)
}
/* Copy CA certificate to /etc/icinga2/pki */
String ca_path = PkiUtility::GetLocalCaPath();
String ca_path = ApiListener::GetCaDir();
String ca = ca_path + "/ca.crt";
String ca_key = ca_path + "/ca.key";
String target_ca = pki_path + "/ca.crt";

84
lib/cli/calistcommand.cpp Normal file
View File

@ -0,0 +1,84 @@
/******************************************************************************
* Icinga 2 *
* Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* of the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the Free Software Foundation *
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
******************************************************************************/
#include "cli/calistcommand.hpp"
#include "remote/apilistener.hpp"
#include "remote/pkiutility.hpp"
#include "base/logger.hpp"
#include "base/application.hpp"
#include "base/tlsutility.hpp"
#include "base/json.hpp"
using namespace icinga;
namespace po = boost::program_options;
REGISTER_CLICOMMAND("ca/list", CAListCommand);
String CAListCommand::GetDescription(void) const
{
return "Lists all certificate signing requests.";
}
String CAListCommand::GetShortDescription(void) const
{
return "lists all certificate signing requests";
}
void CAListCommand::InitParameters(boost::program_options::options_description& visibleDesc,
boost::program_options::options_description& hiddenDesc) const
{
visibleDesc.add_options()
("json", "encode output as JSON")
;
}
/**
* The entry point for the "ca list" CLI command.
*
* @returns An exit status.
*/
int CAListCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
{
Dictionary::Ptr requests = PkiUtility::GetCertificateRequests();
if (vm.count("json"))
std::cout << JsonEncode(requests);
else {
ObjectLock olock(requests);
std::cout << "Fingerprint | Timestamp | Signed | Subject\n";
std::cout << "-----------------------------------------------------------------|--------------------------|--------|--------\n";
for (auto& kv : requests) {
Dictionary::Ptr request = kv.second;
std::cout << kv.first
<< " | "
/* << Utility::FormatDateTime("%Y/%m/%d %H:%M:%S", request->Get("timestamp")) */
<< request->Get("timestamp")
<< " | "
<< (request->Contains("cert_response") ? "*" : " ") << " "
<< " | "
<< request->Get("subject")
<< "\n";
}
}
return 0;
}

50
lib/cli/calistcommand.hpp Normal file
View File

@ -0,0 +1,50 @@
/******************************************************************************
* Icinga 2 *
* Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* of the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the Free Software Foundation *
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
******************************************************************************/
#ifndef CALISTCOMMAND_H
#define CALISTCOMMAND_H
#include "cli/clicommand.hpp"
namespace icinga
{
/**
* The "ca list" command.
*
* @ingroup cli
*/
class CAListCommand : public CLICommand
{
public:
DECLARE_PTR_TYPEDEFS(CAListCommand);
virtual String GetDescription(void) const override;
virtual String GetShortDescription(void) const override;
virtual void InitParameters(boost::program_options::options_description& visibleDesc,
boost::program_options::options_description& hiddenDesc) const override;
virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
private:
static void PrintRequest(const String& requestFile);
};
}
#endif /* CALISTCOMMAND_H */

105
lib/cli/casigncommand.cpp Normal file
View File

@ -0,0 +1,105 @@
/******************************************************************************
* Icinga 2 *
* Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* of the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the Free Software Foundation *
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
******************************************************************************/
#include "cli/casigncommand.hpp"
#include "remote/apilistener.hpp"
#include "base/logger.hpp"
#include "base/application.hpp"
#include "base/tlsutility.hpp"
using namespace icinga;
REGISTER_CLICOMMAND("ca/sign", CASignCommand);
String CASignCommand::GetDescription(void) const
{
return "Signs an outstanding certificate request.";
}
String CASignCommand::GetShortDescription(void) const
{
return "signs an outstanding certificate request";
}
int CASignCommand::GetMinArguments(void) const
{
return 1;
}
ImpersonationLevel CASignCommand::GetImpersonationLevel(void) const
{
return ImpersonateIcinga;
}
/**
* The entry point for the "ca sign" CLI command.
*
* @returns An exit status.
*/
int CASignCommand::Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const
{
String requestFile = ApiListener::GetCertificateRequestsDir() + "/" + ap[0] + ".json";
if (!Utility::PathExists(requestFile)) {
Log(LogCritical, "cli")
<< "No request exists for fingerprint '" << ap[0] << "'.";
return 1;
}
Dictionary::Ptr request = Utility::LoadJsonFile(requestFile);
if (!request)
return 1;
String certRequestText = request->Get("cert_request");
boost::shared_ptr<X509> certRequest = StringToCertificate(certRequestText);
if (!certRequest) {
Log(LogCritical, "cli", "Certificate request is invalid. Could not parse X.509 certificate for the 'cert_request' attribute.");
return 1;
}
boost::shared_ptr<X509> certResponse = CreateCertIcingaCA(certRequest);
BIO *out = BIO_new(BIO_s_mem());
X509_NAME_print_ex(out, X509_get_subject_name(certRequest.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
char *data;
long length;
length = BIO_get_mem_data(out, &data);
String subject = String(data, data + length);
BIO_free(out);
if (!certResponse) {
Log(LogCritical, "cli")
<< "Could not sign certificate for '" << subject << "'.";
return 1;
}
request->Set("cert_response", CertificateToString(certResponse));
Utility::SaveJsonFile(requestFile, 0600, request);
Log(LogInformation, "cli")
<< "Signed certificate for '" << subject << "'.";
return 0;
}

47
lib/cli/casigncommand.hpp Normal file
View File

@ -0,0 +1,47 @@
/******************************************************************************
* Icinga 2 *
* Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* of the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the Free Software Foundation *
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
******************************************************************************/
#ifndef CASIGNCOMMAND_H
#define CASIGNCOMMAND_H
#include "cli/clicommand.hpp"
namespace icinga
{
/**
* The "ca sign" command.
*
* @ingroup cli
*/
class CASignCommand : public CLICommand
{
public:
DECLARE_PTR_TYPEDEFS(CASignCommand);
virtual String GetDescription(void) const override;
virtual String GetShortDescription(void) const override;
virtual int GetMinArguments(void) const override;
virtual ImpersonationLevel GetImpersonationLevel(void) const override;
virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
};
}
#endif /* CASIGNCOMMAND_H */

View File

@ -20,8 +20,9 @@
#include "cli/nodesetupcommand.hpp"
#include "cli/nodeutility.hpp"
#include "cli/featureutility.hpp"
#include "cli/pkiutility.hpp"
#include "cli/apisetuputility.hpp"
#include "remote/apilistener.hpp"
#include "remote/pkiutility.hpp"
#include "base/logger.hpp"
#include "base/console.hpp"
#include "base/application.hpp"
@ -130,14 +131,14 @@ int NodeSetupCommand::SetupMaster(const boost::program_options::variables_map& v
cn = vm["cn"].as<std::string>();
/* check whether the user wants to generate a new certificate or not */
String existing_path = PkiUtility::GetPkiPath() + "/" + cn + ".crt";
String existingPath = ApiListener::GetCertsDir() + "/" + cn + ".crt";
Log(LogInformation, "cli")
<< "Checking for existing certificates for common name '" << cn << "'...";
<< "Checking in existing certificates for common name '" << cn << "'...";
if (Utility::PathExists(existing_path)) {
if (Utility::PathExists(existingPath)) {
Log(LogWarning, "cli")
<< "Certificate '" << existing_path << "' for CN '" << cn << "' already exists. Not generating new certificate.";
<< "Certificate '" << existingPath << "' for CN '" << cn << "' already exists. Not generating new certificate.";
} else {
Log(LogInformation, "cli")
<< "Certificates not yet generated. Running 'api setup' now.";
@ -156,13 +157,11 @@ int NodeSetupCommand::SetupMaster(const boost::program_options::variables_map& v
}
/* write zones.conf and update with zone + endpoint information */
Log(LogInformation, "cli", "Generating zone and object configuration.");
NodeUtility::GenerateNodeMasterIcingaConfig();
/* update the ApiListener config - SetupMaster() will always enable it */
Log(LogInformation, "cli", "Updating the APIListener feature.");
String apipath = FeatureUtility::GetFeaturesAvailablePath() + "/api.conf";
@ -175,9 +174,9 @@ int NodeSetupCommand::SetupMaster(const boost::program_options::variables_map& v
<< " * The API listener is used for distributed monitoring setups.\n"
<< " */\n"
<< "object ApiListener \"api\" {\n"
<< " cert_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".crt\"\n"
<< " key_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".key\"\n"
<< " ca_path = SysconfDir + \"/icinga2/pki/ca.crt\"\n";
<< " cert_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".crt\"\n"
<< " key_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".key\"\n"
<< " ca_path = LocalStateDir + \"/lib/icinga2/certs/ca.crt\"\n";
if (vm.count("listen")) {
std::vector<String> tokens;
@ -262,7 +261,8 @@ int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm,
/* require master host information for auto-signing requests */
if (!vm.count("master_host")) {
Log(LogCritical, "cli", "Please pass the master host connection information for auto-signing using '--master_host <host>'");
Log(LogCritical, "cli", "Please pass the master host connection information for auto-signing using '--master_host <host>'. This can also be a direct parent satellite since 2.8.");
return 1;
}
@ -278,13 +278,13 @@ int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm,
master_port = tokens[1];
Log(LogInformation, "cli")
<< "Verifying master host connection information: host '" << master_host << "', port '" << master_port << "'.";
<< "Verifying parent host connection information: host '" << master_host << "', port '" << master_port << "'.";
/* trusted cert must be passed (retrieved by the user with 'pki save-cert' before) */
if (!vm.count("trustedcert")) {
Log(LogCritical, "cli")
<< "Please pass the trusted cert retrieved from the master\n"
<< "Please pass the trusted cert retrieved from the parent node (master or satellite)\n"
<< "(Hint: 'icinga2 pki save-cert --host <masterhost> --port <5665> --key local.key --cert local.crt --trustedcert master.crt').";
return 1;
}
@ -305,7 +305,7 @@ int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm,
/* pki request a signed certificate from the master */
String pki_path = PkiUtility::GetPkiPath();
String pki_path = ApiListener::GetCertsDir();
Utility::MkDirP(pki_path, 0700);
String user = ScriptGlobal::Get("RunAsUser");
@ -336,10 +336,10 @@ int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm,
<< "Cannot set ownership for user '" << user << "' group '" << group << "' on file '" << key << "'. Verify it yourself!";
}
Log(LogInformation, "cli", "Requesting a signed certificate from the master.");
Log(LogInformation, "cli", "Requesting a signed certificate from the parent Icinga node.");
if (PkiUtility::RequestCertificate(master_host, master_port, key, cert, ca, trustedcert, ticket) != 0) {
Log(LogCritical, "cli", "Failed to request certificate from Icinga 2 master.");
Log(LogCritical, "cli", "Failed to request certificate from parent Icinga node.");
return 1;
}
@ -379,9 +379,9 @@ int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm,
<< " * The API listener is used for distributed monitoring setups.\n"
<< " */\n"
<< "object ApiListener \"api\" {\n"
<< " cert_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".crt\"\n"
<< " key_path = SysconfDir + \"/icinga2/pki/\" + NodeName + \".key\"\n"
<< " ca_path = SysconfDir + \"/icinga2/pki/ca.crt\"\n";
<< " cert_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".crt\"\n"
<< " key_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".key\"\n"
<< " ca_path = LocalStateDir + \"/lib/icinga2/certs/ca.crt\"\n";
if (vm.count("listen")) {
std::vector<String> tokens;
@ -406,7 +406,6 @@ int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm,
fp << " accept_commands = false\n";
fp << "\n"
<< " ticket_salt = TicketSalt\n"
<< "}\n";
fp.close();
@ -431,7 +430,7 @@ int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm,
/* update constants.conf with NodeName = CN */
if (cn != Utility::GetFQDN()) {
Log(LogWarning, "cli")
<< "CN '" << cn << "' does not match the default FQDN '" << Utility::GetFQDN() << "'. Requires update for NodeName constant in constants.conf!";
<< "CN '" << cn << "' does not match the default FQDN '" << Utility::GetFQDN() << "'. Requires an update for the NodeName constant in constants.conf!";
}
Log(LogInformation, "cli", "Updating constants.conf.");
@ -441,8 +440,33 @@ int NodeSetupCommand::SetupNode(const boost::program_options::variables_map& vm,
NodeUtility::UpdateConstant("NodeName", cn);
NodeUtility::UpdateConstant("ZoneName", vm["zone"].as<std::string>());
/* tell the user to reload icinga2 */
String ticketPath = ApiListener::GetCertsDir() + "/ticket";
String tempTicketPath = Utility::CreateTempFile(ticketPath + ".XXXXXX", 0600, fp);
if (!Utility::SetFileOwnership(tempTicketPath, user, group)) {
Log(LogWarning, "cli")
<< "Cannot set ownership for user '" << user
<< "' group '" << group
<< "' on file '" << tempTicketPath << "'. Verify it yourself!";
}
fp << ticket;
fp.close();
#ifdef _WIN32
_unlink(ticketPath.CStr());
#endif /* _WIN32 */
if (rename(tempTicketPath.CStr(), ticketPath.CStr()) < 0) {
BOOST_THROW_EXCEPTION(posix_error()
<< boost::errinfo_api_function("rename")
<< boost::errinfo_errno(errno)
<< boost::errinfo_file_name(tempTicketPath));
}
/* tell the user to reload icinga2 */
Log(LogInformation, "cli", "Make sure to restart Icinga 2.");
return 0;

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,12 @@ public:
virtual int GetMaxArguments(void) const override;
virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
virtual ImpersonationLevel GetImpersonationLevel(void) const override;
virtual void InitParameters(boost::program_options::options_description& visibleDesc,
boost::program_options::options_description& hiddenDesc) const override;
private:
int ClientSetup(void) const;
int MasterSetup(void) const;
};
}

View File

@ -18,7 +18,7 @@
******************************************************************************/
#include "cli/pkinewcacommand.hpp"
#include "cli/pkiutility.hpp"
#include "remote/pkiutility.hpp"
#include "base/logger.hpp"
using namespace icinga;

View File

@ -18,7 +18,7 @@
******************************************************************************/
#include "cli/pkinewcertcommand.hpp"
#include "cli/pkiutility.hpp"
#include "remote/pkiutility.hpp"
#include "base/logger.hpp"
using namespace icinga;

View File

@ -18,7 +18,7 @@
******************************************************************************/
#include "cli/pkirequestcommand.hpp"
#include "cli/pkiutility.hpp"
#include "remote/pkiutility.hpp"
#include "base/logger.hpp"
#include "base/tlsutility.hpp"
#include <iostream>
@ -95,17 +95,16 @@ int PKIRequestCommand::Run(const boost::program_options::variables_map& vm, cons
return 1;
}
if (!vm.count("ticket")) {
Log(LogCritical, "cli", "Ticket (--ticket) must be specified.");
return 1;
}
String port = "5665";
String ticket;
if (vm.count("port"))
port = vm["port"].as<std::string>();
if (vm.count("ticket"))
ticket = vm["ticket"].as<std::string>();
return PkiUtility::RequestCertificate(vm["host"].as<std::string>(), port, vm["key"].as<std::string>(),
vm["cert"].as<std::string>(), vm["ca"].as<std::string>(), GetX509Certificate(vm["trustedcert"].as<std::string>()),
vm["ticket"].as<std::string>());
ticket);
}

View File

@ -18,9 +18,10 @@
******************************************************************************/
#include "cli/pkisavecertcommand.hpp"
#include "cli/pkiutility.hpp"
#include "remote/pkiutility.hpp"
#include "base/logger.hpp"
#include "base/tlsutility.hpp"
#include "base/console.hpp"
using namespace icinga;
namespace po = boost::program_options;
@ -77,13 +78,26 @@ int PKISaveCertCommand::Run(const boost::program_options::variables_map& vm, con
return 1;
}
boost::shared_ptr<X509> cert =
PkiUtility::FetchCert(vm["host"].as<std::string>(), vm["port"].as<std::string>());
String host = vm["host"].as<std::string>();
String port = vm["port"].as<std::string>();
Log(LogInformation, "cli")
<< "Retrieving X.509 certificate for '" << host << ":" << port << "'.";
boost::shared_ptr<X509> cert = PkiUtility::FetchCert(host, port);
if (!cert) {
Log(LogCritical, "cli", "Failed to fetch certificate from host");
Log(LogCritical, "cli", "Failed to fetch certificate from host.");
return 1;
}
std::cout << PkiUtility::GetCertificateInformation(cert) << "\n";
std::cout << ConsoleColorTag(Console_ForegroundRed)
<< "***\n"
<< "*** You have to ensure that this certificate actually matches the parent\n"
<< "*** instance's certificate in order to avoid man-in-the-middle attacks.\n"
<< "***\n\n"
<< ConsoleColorTag(Console_Normal);
return PkiUtility::WriteCert(cert, vm["trustedcert"].as<std::string>());
}

View File

@ -18,7 +18,7 @@
******************************************************************************/
#include "cli/pkisigncsrcommand.hpp"
#include "cli/pkiutility.hpp"
#include "remote/pkiutility.hpp"
#include "base/logger.hpp"
using namespace icinga;

View File

@ -18,7 +18,7 @@
******************************************************************************/
#include "cli/pkiticketcommand.hpp"
#include "cli/pkiutility.hpp"
#include "remote/pkiutility.hpp"
#include "cli/variableutility.hpp"
#include "base/logger.hpp"
#include <iostream>

View File

@ -27,6 +27,7 @@
#include "icinga/notificationcommand.hpp"
#include "remote/apiaction.hpp"
#include "remote/apilistener.hpp"
#include "remote/pkiutility.hpp"
#include "remote/httputility.hpp"
#include "base/utility.hpp"
#include "base/convert.hpp"

View File

@ -28,8 +28,9 @@ set(remote_SOURCES
configstageshandler.cpp createobjecthandler.cpp deleteobjecthandler.cpp
endpoint.cpp endpoint.thpp eventshandler.cpp eventqueue.cpp filterutility.cpp
httpchunkedencoding.cpp httpclientconnection.cpp httpserverconnection.cpp httphandler.cpp httprequest.cpp httpresponse.cpp
httputility.cpp infohandler.cpp jsonrpc.cpp jsonrpcconnection.cpp jsonrpcconnection-heartbeat.cpp
httputility.cpp infohandler.cpp jsonrpc.cpp jsonrpcconnection.cpp jsonrpcconnection-heartbeat.cpp jsonrpcconnection-pki.cpp
messageorigin.cpp modifyobjecthandler.cpp statushandler.cpp objectqueryhandler.cpp templatequeryhandler.cpp
pkiutility.cpp
typequeryhandler.cpp url.cpp variablequeryhandler.cpp zone.cpp zone.thpp
)

View File

@ -55,6 +55,26 @@ ApiListener::ApiListener(void)
m_SyncQueue.SetName("ApiListener, SyncQueue");
}
String ApiListener::GetApiDir(void)
{
return Application::GetLocalStateDir() + "/lib/icinga2/api/";
}
String ApiListener::GetCertsDir(void)
{
return Application::GetLocalStateDir() + "/lib/icinga2/certs/";
}
String ApiListener::GetCaDir(void)
{
return Application::GetLocalStateDir() + "/lib/icinga2/ca/";
}
String ApiListener::GetCertificateRequestsDir(void)
{
return Application::GetLocalStateDir() + "/lib/icinga2/certificate-requests/";
}
void ApiListener::OnConfigLoaded(void)
{
if (m_Instance)
@ -81,8 +101,15 @@ void ApiListener::OnConfigLoaded(void)
Log(LogInformation, "ApiListener")
<< "My API identity: " << GetIdentity();
UpdateSSLContext();
}
void ApiListener::UpdateSSLContext(void)
{
boost::shared_ptr<SSL_CTX> context;
try {
m_SSLContext = MakeSSLContext(GetCertPath(), GetKeyPath(), GetCaPath());
context = MakeSSLContext(GetCertPath(), GetKeyPath(), GetCaPath());
} catch (const std::exception&) {
BOOST_THROW_EXCEPTION(ScriptError("Cannot make SSL context for cert path: '"
+ GetCertPath() + "' key path: '" + GetKeyPath() + "' ca path: '" + GetCaPath() + "'.", GetDebugInfo()));
@ -90,7 +117,7 @@ void ApiListener::OnConfigLoaded(void)
if (!GetCrlPath().IsEmpty()) {
try {
AddCRLToSSLContext(m_SSLContext, GetCrlPath());
AddCRLToSSLContext(context, GetCrlPath());
} catch (const std::exception&) {
BOOST_THROW_EXCEPTION(ScriptError("Cannot add certificate revocation list to SSL context for crl path: '"
+ GetCrlPath() + "'.", GetDebugInfo()));
@ -99,7 +126,7 @@ void ApiListener::OnConfigLoaded(void)
if (!GetCipherList().IsEmpty()) {
try {
SetCipherListToSSLContext(m_SSLContext, GetCipherList());
SetCipherListToSSLContext(context, GetCipherList());
} catch (const std::exception&) {
BOOST_THROW_EXCEPTION(ScriptError("Cannot set cipher list to SSL context for cipher list: '"
+ GetCipherList() + "'.", GetDebugInfo()));
@ -108,11 +135,23 @@ void ApiListener::OnConfigLoaded(void)
if (!GetTlsProtocolmin().IsEmpty()){
try {
SetTlsProtocolminToSSLContext(m_SSLContext, GetTlsProtocolmin());
SetTlsProtocolminToSSLContext(context, GetTlsProtocolmin());
} catch (const std::exception&) {
BOOST_THROW_EXCEPTION(ScriptError("Cannot set minimum TLS protocol version to SSL context with tls_protocolmin: '" + GetTlsProtocolmin() + "'.", GetDebugInfo()));
}
}
m_SSLContext = context;
for (const Endpoint::Ptr& endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
for (const JsonRpcConnection::Ptr& client : endpoint->GetClients()) {
client->Disconnect();
}
}
for (const JsonRpcConnection::Ptr& client : m_AnonymousClients) {
client->Disconnect();
}
}
void ApiListener::OnAllConfigLoaded(void)
@ -165,6 +204,12 @@ void ApiListener::Start(bool runtimeCreated)
m_AuthorityTimer->SetInterval(30);
m_AuthorityTimer->Start();
m_CleanupCertificateRequestsTimer = new Timer();
m_CleanupCertificateRequestsTimer->OnTimerExpired.connect(boost::bind(&ApiListener::CleanupCertificateRequestsTimerHandler, this));
m_CleanupCertificateRequestsTimer->SetInterval(3600);
m_CleanupCertificateRequestsTimer->Start();
m_CleanupCertificateRequestsTimer->Reschedule(0);
OnMasterChanged(true);
}
@ -184,11 +229,6 @@ ApiListener::Ptr ApiListener::GetInstance(void)
return m_Instance;
}
boost::shared_ptr<SSL_CTX> ApiListener::GetSSLContext(void) const
{
return m_SSLContext;
}
Endpoint::Ptr ApiListener::GetMaster(void) const
{
Zone::Ptr zone = Zone::GetLocalZone();
@ -395,7 +435,6 @@ void ApiListener::NewClientHandlerInternal(const Socket::Ptr& client, const Stri
Log(LogWarning, "ApiListener")
<< "Certificate validation failed for endpoint '" << hostname
<< "': " << tlsStream->GetVerifyError();
return;
}
}
@ -478,6 +517,18 @@ void ApiListener::SyncClient(const JsonRpcConnection::Ptr& aclient, const Endpoi
endpoint->SetSyncing(true);
}
Zone::Ptr myZone = Zone::GetLocalZone();
if (myZone->GetParent() == eZone) {
Log(LogInformation, "ApiListener")
<< "Requesting new certificate for this Icinga instance from endpoint '" << endpoint->GetName() << "'.";
JsonRpcConnection::SendCertificateRequest(aclient, MessageOrigin::Ptr(), String());
if (Utility::PathExists(ApiListener::GetCertificateRequestsDir()))
Utility::Glob(ApiListener::GetCertificateRequestsDir() + "/*.json", boost::bind(&JsonRpcConnection::SendCertificateRequest, aclient, MessageOrigin::Ptr(), _1), GlobFile);
}
/* Make sure that the config updates are synced
* before the logs are replayed.
*/
@ -597,7 +648,6 @@ void ApiListener::ApiTimerHandler(void)
<< "Setting log position for identity '" << endpoint->GetName() << "': "
<< Utility::FormatDateTime("%Y/%m/%d %H:%M:%S", ts);
}
}
void ApiListener::ApiReconnectTimerHandler(void)
@ -669,6 +719,33 @@ void ApiListener::ApiReconnectTimerHandler(void)
<< "Connected endpoints: " << Utility::NaturalJoin(names);
}
static void CleanupCertificateRequest(const String& path, double expiryTime)
{
#ifndef _WIN32
struct stat statbuf;
if (lstat(path.CStr(), &statbuf) < 0)
return;
#else /* _WIN32 */
struct _stat statbuf;
if (_stat(path.CStr(), &statbuf) < 0)
return;
#endif /* _WIN32 */
if (statbuf.st_mtime < expiryTime)
(void) unlink(path.CStr());
}
void ApiListener::CleanupCertificateRequestsTimerHandler(void)
{
String requestsDir = GetCertificateRequestsDir();
if (Utility::PathExists(requestsDir)) {
/* remove certificate requests that are older than a week */
double expiryTime = Utility::GetTime() - 7 * 24 * 60 * 60;
Utility::Glob(requestsDir + "/*.json", boost::bind(&CleanupCertificateRequest, _1, expiryTime), GlobFile);
}
}
void ApiListener::RelayMessage(const MessageOrigin::Ptr& origin,
const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log)
{
@ -863,11 +940,6 @@ void ApiListener::SyncRelayMessage(const MessageOrigin::Ptr& origin,
PersistMessage(message, secobj);
}
String ApiListener::GetApiDir(void)
{
return Application::GetLocalStateDir() + "/lib/icinga2/api/";
}
/* must hold m_LogLock */
void ApiListener::OpenLogFile(void)
{

View File

@ -59,17 +59,20 @@ public:
ApiListener(void);
static ApiListener::Ptr GetInstance(void);
static String GetApiDir(void);
static String GetCertsDir(void);
static String GetCaDir(void);
static String GetCertificateRequestsDir(void);
boost::shared_ptr<SSL_CTX> GetSSLContext(void) const;
void UpdateSSLContext(void);
static ApiListener::Ptr GetInstance(void);
Endpoint::Ptr GetMaster(void) const;
bool IsMaster(void) const;
Endpoint::Ptr GetLocalEndpoint(void) const;
static String GetApiDir(void);
void SyncSendMessage(const Endpoint::Ptr& endpoint, const Dictionary::Ptr& message);
void RelayMessage(const MessageOrigin::Ptr& origin, const ConfigObject::Ptr& secobj, const Dictionary::Ptr& message, bool log);
@ -117,12 +120,14 @@ private:
Timer::Ptr m_Timer;
Timer::Ptr m_ReconnectTimer;
Timer::Ptr m_AuthorityTimer;
Timer::Ptr m_CleanupCertificateRequestsTimer;
Endpoint::Ptr m_LocalEndpoint;
static ApiListener::Ptr m_Instance;
void ApiTimerHandler(void);
void ApiReconnectTimerHandler(void);
void CleanupCertificateRequestsTimerHandler(void);
bool AddListener(const String& node, const String& service);
void AddConnection(const Endpoint::Ptr& endpoint);

View File

@ -0,0 +1,391 @@
/******************************************************************************
* Icinga 2 *
* Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/) *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* of the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the Free Software Foundation *
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
******************************************************************************/
#include "remote/jsonrpcconnection.hpp"
#include "remote/apilistener.hpp"
#include "remote/apifunction.hpp"
#include "remote/jsonrpc.hpp"
#include "base/configtype.hpp"
#include "base/objectlock.hpp"
#include "base/utility.hpp"
#include "base/logger.hpp"
#include "base/exception.hpp"
#include "base/convert.hpp"
#include <boost/thread/once.hpp>
#include <boost/regex.hpp>
#include <fstream>
using namespace icinga;
static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
static Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(UpdateCertificate, pki, &UpdateCertificateHandler);
Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
if (!params)
return Empty;
String certText = params->Get("cert_request");
boost::shared_ptr<X509> cert;
Dictionary::Ptr result = new Dictionary();
/* Use the presented client certificate if not provided. */
if (certText.IsEmpty())
cert = origin->FromClient->GetStream()->GetPeerCertificate();
else
cert = StringToCertificate(certText);
ApiListener::Ptr listener = ApiListener::GetInstance();
boost::shared_ptr<X509> cacert = GetX509Certificate(listener->GetCaPath());
String cn = GetCertificateCN(cert);
bool signedByCA = VerifyCertificate(cacert, cert);
Log(LogInformation, "JsonRpcConnection")
<< "Received certificate request for CN '" << cn << "'"
<< (signedByCA ? "" : " not") << " signed by our CA.";
if (signedByCA) {
time_t now;
time(&now);
/* auto-renew all certificates which were created before 2017 to force an update of the CA,
* because Icinga versions older than 2.4 sometimes create certificates with an invalid
* serial number. */
time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
time_t renewalStart = now + 30 * 24 * 60 * 60;
if (X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1) {
Log(LogInformation, "JsonRpcConnection")
<< "The certificate for CN '" << cn << "' cannot be renewed yet.";
result->Set("status_code", 1);
result->Set("error", "The certificate for CN '" + cn + "' cannot be renewed yet.");
return result;
}
}
unsigned int n;
unsigned char digest[EVP_MAX_MD_SIZE];
if (!X509_digest(cert.get(), EVP_sha256(), digest, &n)) {
result->Set("status_code", 1);
result->Set("error", "Could not calculate fingerprint for the X509 certificate for CN '" + cn + "'.");
Log(LogWarning, "JsonRpcConnection")
<< "Could not calculate fingerprint for the X509 certificate requested for CN '"
<< cn << "'.";
return result;
}
char certFingerprint[EVP_MAX_MD_SIZE*2+1];
for (unsigned int i = 0; i < n; i++)
sprintf(certFingerprint + 2 * i, "%02x", digest[i]);
result->Set("fingerprint_request", certFingerprint);
String requestDir = ApiListener::GetCertificateRequestsDir();
String requestPath = requestDir + "/" + certFingerprint + ".json";
result->Set("ca", CertificateToString(cacert));
JsonRpcConnection::Ptr client = origin->FromClient;
/* If we already have a signed certificate request, send it to the client. */
if (Utility::PathExists(requestPath)) {
Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
String certResponse = request->Get("cert_response");
if (!certResponse.IsEmpty()) {
Log(LogInformation, "JsonRpcConnection")
<< "Sending certificate response for CN '" << cn
<< "' to endpoint '" << client->GetIdentity() << "'.";
result->Set("cert", certResponse);
result->Set("status_code", 0);
Dictionary::Ptr message = new Dictionary();
message->Set("jsonrpc", "2.0");
message->Set("method", "pki::UpdateCertificate");
message->Set("params", result);
JsonRpc::SendMessage(client->GetStream(), message);
return result;
}
}
boost::shared_ptr<X509> newcert;
boost::shared_ptr<EVP_PKEY> pubkey;
X509_NAME *subject;
Dictionary::Ptr message;
String ticket;
/* Check whether we are a signing instance or we
* must delay the signing request.
*/
if (!Utility::PathExists(GetIcingaCADir() + "/ca.key"))
goto delayed_request;
if (!signedByCA) {
String salt = listener->GetTicketSalt();
ticket = params->Get("ticket");
/* Auto-signing is disabled by either a) no TicketSalt
* or b) the client did not include a ticket in its request.
*/
if (salt.IsEmpty() || ticket.IsEmpty())
goto delayed_request;
String realTicket = PBKDF2_SHA1(cn, salt, 50000);
if (ticket != realTicket) {
Log(LogWarning, "JsonRpcConnection")
<< "Ticket for CN '" << cn << "' is invalid.";
result->Set("status_code", 1);
result->Set("error", "Invalid ticket for CN '" + cn + "'.");
return result;
}
}
pubkey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(cert.get()), EVP_PKEY_free);
subject = X509_get_subject_name(cert.get());
newcert = CreateCertIcingaCA(pubkey.get(), subject);
/* verify that the new cert matches the CA we're using for the ApiListener;
* this ensures that the CA we have in /var/lib/icinga2/ca matches the one
* we're using for cluster connections (there's no point in sending a client
* a certificate it wouldn't be able to use to connect to us anyway) */
if (!VerifyCertificate(cacert, newcert)) {
Log(LogWarning, "JsonRpcConnection")
<< "The CA in '" << listener->GetCaPath() << "' does not match the CA which Icinga uses "
<< "for its own cluster connections. This is most likely a configuration problem.";
goto delayed_request;
}
/* Send the signed certificate update. */
Log(LogInformation, "JsonRpcConnection")
<< "Sending certificate response for CN '" << cn << "' to endpoint '"
<< client->GetIdentity() << "'" << (!ticket.IsEmpty() ? " (auto-signing ticket)" : "" ) << ".";
result->Set("cert", CertificateToString(newcert));
result->Set("status_code", 0);
message = new Dictionary();
message->Set("jsonrpc", "2.0");
message->Set("method", "pki::UpdateCertificate");
message->Set("params", result);
JsonRpc::SendMessage(client->GetStream(), message);
return result;
delayed_request:
/* Send a delayed certificate signing request. */
Utility::MkDirP(requestDir, 0700);
Dictionary::Ptr request = new Dictionary();
request->Set("cert_request", CertificateToString(cert));
request->Set("ticket", params->Get("ticket"));
Utility::SaveJsonFile(requestPath, 0600, request);
JsonRpcConnection::SendCertificateRequest(JsonRpcConnection::Ptr(), origin, requestPath);
result->Set("status_code", 2);
result->Set("error", "Certificate request for CN '" + cn + "' is pending. Waiting for approval from the parent Icinga instance.");
Log(LogInformation, "JsonRpcConnection")
<< "Certificate request for CN '" << cn << "' is pending. Waiting for approval.";
return result;
}
void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const MessageOrigin::Ptr& origin, const String& path)
{
Dictionary::Ptr message = new Dictionary();
message->Set("jsonrpc", "2.0");
message->Set("method", "pki::RequestCertificate");
ApiListener::Ptr listener = ApiListener::GetInstance();
if (!listener)
return;
Dictionary::Ptr params = new Dictionary();
message->Set("params", params);
/* Path is empty if this is our own request. */
if (path.IsEmpty()) {
String ticketPath = ApiListener::GetCertsDir() + "/ticket";
std::ifstream fp(ticketPath.CStr());
String ticket((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
fp.close();
params->Set("ticket", ticket);
} else {
Dictionary::Ptr request = Utility::LoadJsonFile(path);
if (request->Contains("cert_response"))
return;
params->Set("cert_request", request->Get("cert_request"));
params->Set("ticket", request->Get("ticket"));
}
/* Send the request to a) the connected client
* or b) the local zone and all parents.
*/
if (aclient)
JsonRpc::SendMessage(aclient->GetStream(), message);
else
listener->RelayMessage(origin, Zone::GetLocalZone(), message, false);
}
Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
if (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)) {
Log(LogWarning, "ClusterEvents")
<< "Discarding 'update certificate' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
return Empty;
}
String ca = params->Get("ca");
String cert = params->Get("cert");
ApiListener::Ptr listener = ApiListener::GetInstance();
if (!listener)
return Empty;
boost::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetCertPath());
boost::shared_ptr<X509> newCert = StringToCertificate(cert);
String cn = GetCertificateCN(newCert);
Log(LogInformation, "JsonRpcConnection")
<< "Received certificate update message for CN '" << cn << "'";
/* Check if this is a certificate update for a subordinate instance. */
boost::shared_ptr<EVP_PKEY> oldKey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(oldCert.get()), EVP_PKEY_free);
boost::shared_ptr<EVP_PKEY> newKey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(newCert.get()), EVP_PKEY_free);
if (X509_NAME_cmp(X509_get_subject_name(oldCert.get()), X509_get_subject_name(newCert.get())) != 0 ||
EVP_PKEY_cmp(oldKey.get(), newKey.get()) != 1) {
String certFingerprint = params->Get("fingerprint_request");
/* Validate the fingerprint format. */
boost::regex expr("^[0-9a-f]+$");
if (!boost::regex_match(certFingerprint.GetData(), expr)) {
Log(LogWarning, "JsonRpcConnection")
<< "Endpoint '" << origin->FromClient->GetIdentity() << "' sent an invalid certificate fingerprint: '"
<< certFingerprint << "' for CN '" << cn << "'.";
return Empty;
}
String requestDir = ApiListener::GetCertificateRequestsDir();
String requestPath = requestDir + "/" + certFingerprint + ".json";
/* Save the received signed certificate request to disk. */
if (Utility::PathExists(requestPath)) {
Log(LogInformation, "JsonRpcConnection")
<< "Saved certificate update for CN '" << cn << "'";
Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
request->Set("cert_response", cert);
Utility::SaveJsonFile(requestPath, 0644, request);
}
return Empty;
}
/* Update CA certificate. */
String caPath = listener->GetCaPath();
Log(LogInformation, "JsonRpcConnection")
<< "Updating CA certificate in '" << caPath << "'.";
std::fstream cafp;
String tempCaPath = Utility::CreateTempFile(caPath + ".XXXXXX", 0644, cafp);
cafp << ca;
cafp.close();
#ifdef _WIN32
_unlink(caPath.CStr());
#endif /* _WIN32 */
if (rename(tempCaPath.CStr(), caPath.CStr()) < 0) {
BOOST_THROW_EXCEPTION(posix_error()
<< boost::errinfo_api_function("rename")
<< boost::errinfo_errno(errno)
<< boost::errinfo_file_name(tempCaPath));
}
/* Update signed certificate. */
String certPath = listener->GetCertPath();
Log(LogInformation, "JsonRpcConnection")
<< "Updating client certificate for CN '" << cn << "' in '" << certPath << "'.";
std::fstream certfp;
String tempCertPath = Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp);
certfp << cert;
certfp.close();
#ifdef _WIN32
_unlink(certPath.CStr());
#endif /* _WIN32 */
if (rename(tempCertPath.CStr(), certPath.CStr()) < 0) {
BOOST_THROW_EXCEPTION(posix_error()
<< boost::errinfo_api_function("rename")
<< boost::errinfo_errno(errno)
<< boost::errinfo_file_name(tempCertPath));
}
/* Remove ticket for successful signing request. */
String ticketPath = ApiListener::GetCertsDir() + "/ticket";
if (unlink(ticketPath.CStr()) < 0 && errno != ENOENT) {
BOOST_THROW_EXCEPTION(posix_error()
<< boost::errinfo_api_function("unlink")
<< boost::errinfo_errno(errno)
<< boost::errinfo_file_name(ticketPath));
}
/* Update the certificates at runtime and reconnect all endpoints. */
Log(LogInformation, "JsonRpcConnection")
<< "Updating the client certificate for CN '" << cn << "' at runtime and reconnecting the endpoints.";
listener->UpdateSSLContext();
return Empty;
}

View File

@ -33,8 +33,6 @@ using namespace icinga;
static Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(SetLogPosition, log, &SetLogPositionHandler);
static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
static boost::once_flag l_JsonRpcConnectionOnceFlag = BOOST_ONCE_INIT;
static Timer::Ptr l_JsonRpcConnectionTimeoutTimer;
@ -186,7 +184,21 @@ void JsonRpcConnection::MessageHandler(const String& jsonString)
origin->FromZone = Zone::GetByName(message->Get("originZone"));
}
String method = message->Get("method");
Value vmethod;
if (!message->Get("method", &vmethod)) {
Value vid;
if (!message->Get("id", &vid))
return;
Log(LogWarning, "JsonRpcConnection",
"We received a JSON-RPC response message. This should never happen because we're only ever sending notifications.");
return;
}
String method = vmethod;
Log(LogNotice, "JsonRpcConnection")
<< "Received '" << method << "' message from '" << m_Identity << "'";
@ -202,11 +214,10 @@ void JsonRpcConnection::MessageHandler(const String& jsonString)
resultMessage->Set("result", afunc->Invoke(origin, message->Get("params")));
} catch (const std::exception& ex) {
/* TODO: Add a user readable error message for the remote caller */
resultMessage->Set("error", DiagnosticInformation(ex));
std::ostringstream info;
info << "Error while processing message for identity '" << m_Identity << "'";
String diagInfo = DiagnosticInformation(ex);
resultMessage->Set("error", diagInfo);
Log(LogWarning, "JsonRpcConnection")
<< info.str() << "\n" << DiagnosticInformation(ex);
<< "Error while processing message for identity '" << m_Identity << "'\n" << diagInfo;
}
if (message->Contains("id")) {
@ -276,46 +287,6 @@ Value SetLogPositionHandler(const MessageOrigin::Ptr& origin, const Dictionary::
return Empty;
}
Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
if (!params)
return Empty;
Dictionary::Ptr result = new Dictionary();
if (!origin->FromClient->IsAuthenticated()) {
ApiListener::Ptr listener = ApiListener::GetInstance();
String salt = listener->GetTicketSalt();
if (salt.IsEmpty()) {
result->Set("error", "Ticket salt is not configured.");
return result;
}
String ticket = params->Get("ticket");
String realTicket = PBKDF2_SHA1(origin->FromClient->GetIdentity(), salt, 50000);
if (ticket != realTicket) {
result->Set("error", "Invalid ticket.");
return result;
}
}
boost::shared_ptr<X509> cert = origin->FromClient->GetStream()->GetPeerCertificate();
EVP_PKEY *pubkey = X509_get_pubkey(cert.get());
X509_NAME *subject = X509_get_subject_name(cert.get());
boost::shared_ptr<X509> newcert = CreateCertIcingaCA(pubkey, subject);
result->Set("cert", CertificateToString(newcert));
String cacertfile = GetIcingaCADir() + "/ca.crt";
boost::shared_ptr<X509> cacert = GetX509Certificate(cacertfile);
result->Set("ca", CertificateToString(cacert));
return result;
}
void JsonRpcConnection::CheckLiveness(void)
{
if (m_Seen < Utility::GetTime() - 60 && (!m_Endpoint || !m_Endpoint->GetSyncing())) {

View File

@ -75,6 +75,8 @@ public:
static int GetWorkQueueLength(void);
static double GetWorkQueueRate(void);
static void SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const intrusive_ptr<MessageOrigin>& origin, const String& path);
private:
int m_ID;
String m_Identity;
@ -98,6 +100,8 @@ private:
static void StaticInitialize(void);
static void TimeoutTimerHandler(void);
void CheckLiveness(void);
void CertificateRequestResponseHandler(const Dictionary::Ptr& message);
};
}

View File

@ -17,8 +17,8 @@
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
******************************************************************************/
#include "cli/pkiutility.hpp"
#include "cli/clicommand.hpp"
#include "remote/pkiutility.hpp"
#include "remote/apilistener.hpp"
#include "base/logger.hpp"
#include "base/application.hpp"
#include "base/tlsutility.hpp"
@ -34,19 +34,9 @@
using namespace icinga;
String PkiUtility::GetPkiPath(void)
{
return Application::GetSysconfDir() + "/icinga2/pki";
}
String PkiUtility::GetLocalCaPath(void)
{
return Application::GetLocalStateDir() + "/lib/icinga2/ca";
}
int PkiUtility::NewCa(void)
{
String caDir = GetLocalCaPath();
String caDir = ApiListener::GetCaDir();
String caCertFile = caDir + "/ca.crt";
String caKeyFile = caDir + "/ca.key";
@ -91,7 +81,8 @@ int PkiUtility::SignCsr(const String& csrfile, const String& certfile)
BIO_free(csrbio);
boost::shared_ptr<X509> cert = CreateCertIcingaCA(X509_REQ_get_pubkey(req), X509_REQ_get_subject_name(req));
boost::shared_ptr<EVP_PKEY> pubkey = boost::shared_ptr<EVP_PKEY>(X509_REQ_get_pubkey(req), EVP_PKEY_free);
boost::shared_ptr<X509> cert = CreateCertIcingaCA(pubkey.get(), X509_REQ_get_subject_name(req));
X509_REQ_free(req);
@ -258,11 +249,69 @@ int PkiUtility::RequestCertificate(const String& host, const String& port, const
Dictionary::Ptr result = response->Get("result");
if (result->Contains("ca")) {
try {
StringToCertificate(result->Get("ca"));
} catch (const std::exception& ex) {
Log(LogCritical, "cli")
<< "Could not write CA file: " << DiagnosticInformation(ex, false);
return 1;
}
Log(LogInformation, "cli")
<< "Writing CA certificate to file '" << cafile << "'.";
std::ofstream fpca;
fpca.open(cafile.CStr());
fpca << result->Get("ca");
fpca.close();
if (fpca.fail()) {
Log(LogCritical, "cli")
<< "Could not open CA certificate file '" << cafile << "' for writing.";
return 1;
}
}
if (result->Contains("error")) {
Log(LogCritical, "cli", result->Get("error"));
LogSeverity severity;
Value vstatus;
if (!result->Get("status_code", &vstatus))
vstatus = 1;
int status = vstatus;
if (status == 1)
severity = LogCritical;
else {
severity = LogInformation;
Log(severity, "cli", "!!!!!!");
}
Log(severity, "cli")
<< "!!! " << result->Get("error");
if (status == 1)
return 1;
else {
Log(severity, "cli", "!!!!!!");
return 0;
}
}
try {
StringToCertificate(result->Get("cert"));
} catch (const std::exception& ex) {
Log(LogCritical, "cli")
<< "Could not write certificate file: " << DiagnosticInformation(ex, false);
return 1;
}
Log(LogInformation, "cli")
<< "Writing signed certificate to file '" << certfile << "'.";
std::ofstream fpcert;
fpcert.open(certfile.CStr());
fpcert << result->Get("cert");
@ -274,23 +323,6 @@ int PkiUtility::RequestCertificate(const String& host, const String& port, const
return 1;
}
Log(LogInformation, "cli")
<< "Writing signed certificate to file '" << certfile << "'.";
std::ofstream fpca;
fpca.open(cafile.CStr());
fpca << result->Get("ca");
fpca.close();
if (fpca.fail()) {
Log(LogCritical, "cli")
<< "Could not open CA certificate file '" << cafile << "' for writing.";
return 1;
}
Log(LogInformation, "cli")
<< "Writing CA certificate to file '" << cafile << "'.";
return 0;
}
@ -325,6 +357,9 @@ String PkiUtility::GetCertificateInformation(const boost::shared_ptr<X509>& cert
std::stringstream info;
info << String(data, data + length);
BIO_free(out);
for (unsigned int i = 0; i < diglen; i++) {
info << std::setfill('0') << std::setw(2) << std::uppercase
<< std::hex << static_cast<int>(md[i]) << ' ';
@ -333,3 +368,70 @@ String PkiUtility::GetCertificateInformation(const boost::shared_ptr<X509>& cert
return info.str();
}
static void CollectRequestHandler(const Dictionary::Ptr& requests, const String& requestFile)
{
Dictionary::Ptr request = Utility::LoadJsonFile(requestFile);
if (!request)
return;
Dictionary::Ptr result = new Dictionary();
String fingerprint = Utility::BaseName(requestFile);
fingerprint = fingerprint.SubStr(0, fingerprint.GetLength() - 5);
String certRequestText = request->Get("cert_request");
result->Set("cert_request", certRequestText);
Value vcertResponseText;
if (request->Get("cert_response", &vcertResponseText)) {
String certResponseText = vcertResponseText;
result->Set("cert_response", certResponseText);
}
boost::shared_ptr<X509> certRequest = StringToCertificate(certRequestText);
/* XXX (requires OpenSSL >= 1.0.0)
time_t now;
time(&now);
ASN1_TIME *tm = ASN1_TIME_adj(NULL, now, 0, 0);
int day, sec;
ASN1_TIME_diff(&day, &sec, tm, X509_get_notBefore(certRequest.get()));
result->Set("timestamp", static_cast<double>(now) + day * 24 * 60 * 60 + sec); */
BIO *out = BIO_new(BIO_s_mem());
ASN1_TIME_print(out, X509_get_notBefore(certRequest.get()));
char *data;
long length;
length = BIO_get_mem_data(out, &data);
result->Set("timestamp", String(data, data + length));
BIO_free(out);
out = BIO_new(BIO_s_mem());
X509_NAME_print_ex(out, X509_get_subject_name(certRequest.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
length = BIO_get_mem_data(out, &data);
result->Set("subject", String(data, data + length));
BIO_free(out);
requests->Set(fingerprint, result);
}
Dictionary::Ptr PkiUtility::GetCertificateRequests(void)
{
Dictionary::Ptr requests = new Dictionary();
String requestDir = ApiListener::GetCertificateRequestsDir();
if (Utility::PathExists(requestDir))
Utility::Glob(requestDir + "/*.json", boost::bind(&CollectRequestHandler, requests, _1), GlobFile);
return requests;
}

View File

@ -20,8 +20,7 @@
#ifndef PKIUTILITY_H
#define PKIUTILITY_H
#include "base/i2-base.hpp"
#include "cli/i2-cli.hpp"
#include "remote/i2-remote.hpp"
#include "base/dictionary.hpp"
#include "base/string.hpp"
#include <openssl/x509v3.h>
@ -30,14 +29,11 @@ namespace icinga
{
/**
* @ingroup cli
* @ingroup remote
*/
class I2_CLI_API PkiUtility
class I2_REMOTE_API PkiUtility
{
public:
static String GetPkiPath(void);
static String GetLocalCaPath(void);
static int NewCa(void);
static int NewCert(const String& cn, const String& keyfile, const String& csrfile, const String& certfile);
static int SignCsr(const String& csrfile, const String& certfile);
@ -46,8 +42,9 @@ public:
static int GenTicket(const String& cn, const String& salt, std::ostream& ticketfp);
static int RequestCertificate(const String& host, const String& port, const String& keyfile,
const String& certfile, const String& cafile, const boost::shared_ptr<X509>& trustedcert,
const String& ticket);
const String& ticket = String());
static String GetCertificateInformation(const boost::shared_ptr<X509>& certificate);
static Dictionary::Ptr GetCertificateRequests(void);
private:
PkiUtility(void);