Client server communication
Starting cf-serverd
sets up a line of communication between
hosts. This daemon authenticates requests from the network and processes them
according to rules specified in the
server control
body and server bundles
containing access
promises.
The server can allow the network to access files or to execute CFEngine:
The only contact
cf-agent
makes to the server is via remote copy requests. It does not and cannot grant any access to a system from the network. It is only able to request access to files on the remote server.cf-runagent
can be used to runcf-agent
on a number of remote hosts.
Unlike other approaches to automation, CFEngine does not rely on SSH key authentication and configuring trust, the communication between hosts is very structured and also able to react to availability issues. This means that you don't have to arrange login credentials to get CFEngine to work. If large portions of your network stop working, then CFEngine on each individual host understands how to keep on running and delivering promises.
In particular, if the network is not working, CFEngine agents skip downloading new promises and continue with what they already have. CFEngine was specifically designed to be resilient against connectivity issues network failure may be in question. CFEngine is fault tolerant and opportunistic.
Connecting to server
In order to connect to the CFEngine server you need:
- A public-private key pair. It is automatically generated during package
installation or during bootstrap. To manually create a key pair,
run
cf-key
. - Network connectivity with an IPv4 or IPv6 address.
- Permission to connect to the server.
The
server control
body must grant access to your computer and public key by name or IP address, by listing it in the appropriate access lists (see below). - Mutual key trust.
Your public key must be trusted by the server, and you must trust the server's
public key. The first part is established by having the
trustkeysfrom
setting open on the server for the first connection of the agent. It should be closed later to avoid trusting new agents. The second part is established by bootstrapping the agent to the hub, or by executing acopy_from
files promise usingtrustkey=>"true"
. - Permission to access something.
Your host name or IP address must be mentioned in an
access
promise inside a server bundle, made by the file that you are trying to access.
If all of the above criteria are met, connection will be established and data will be transferred between client and server. The client can only send short requests, following the CFEngine protocol. The server can return data in a variety of forms, usually files, but sometimes console output.
Bootstrapping
Bootstrap is the manual first run of cf-agent that establishes
communication with the policy server.
Bootstrapping executes the failsafe.cf
policy that connects to
the server, establishes trust to the server's key, and that starts the
CFEngine daemon processes cf-execd
, cf-serverd
and cf-monitord
.
The host that other hosts are bootstrapped to
automatically assumes the role of policy server.
You should bootstrap the policy server first to itself:
$ /var/cfengine/bin/cf-agent --bootstrap [public IP of localhost]
Then execute the same step (using the exact same IP) on all hosts that should pull policy from that server. CFEngine will create keys if there are none present, and exchange those to establish trust.
CFEngine will output diagnostic information upon bootstrap. In case of error,
investigate the access
promises the server is making (run cf-serverd
in
verbose mode on the policy hub for more informative messages). Note that
by default, CFEngine's server daemon cf-serverd
trusts incoming connections
from hosts within the same /16
subnet.
After a host has been bootstrapped, the text file policy_server.dat
in
the CFEngine installation contains the IP address of the policy server.
Key exchange
The key exchange model used by CFEngine is based on that used by OpenSSH. It is a peer to peer exchange model, not a central certificate authority model. This means that there are no scalability bottlenecks (at least by design, though you might introduce your own if you go for an overly centralized architecture).
Key exchange is handled automatically by CFEngine and all you need to do is to
decide which keys to trust. The server (cf-serverd
) trusts new keys only
from addresses in trustkeysfrom
. Once a key has been
accepted you should close down trustkeysfrom
list. Then, even if a malicious peer
is spoofing an allowed IP address, its unknown key will be denied.
Once you have arranged for the right to connect to the server, you must decide
which hosts will have access to which files. This is done with access
promises.
bundle server access_rules()
{
access:
"/path/file"
admit => { "127.0.0.1", "127.0.0.2", "127.0.0.3" },
deny => { "192.168.0.0/8" };
}
On the client side, i.e. cf-runagent
and cf-agent
, there are three issues:
- Choosing which server to connect to.
- Trusting the key of any previously unknown servers
- Choosing whether data transfers should be encrypted (with
encrypt
) - not applicable if you are using newprotocol_version
.
There are two ways of managing trust of server keys by a client. One is an
automated option, setting the option trustkey
in a copy_from
files promise, e.g.
body copy_from example
{
# .. other settings ..
trustkey => "true";
}
Another way is to run cf-runagent
in interactive mode. When you run
cf-runagent
, unknown server keys are offered to you interactively (as with
ssh
) for you to accept or deny manually:
$ WARNING - You do not have a public key from host ubik.iu.hio.no = 128.39.74.25
$ Do you want to accept one on trust? (yes/no)
-->
Once public keys have been exchanged from client to server and from server to client, the issue of trust is solved according to public key authentication schemes. You only need to worry about trust when one side of a connection has never seen the other side before.
Time windows (races)
All security is based on a moment of trust that is granted by a user at some point in time – and is assumed thereafter (once given, hard to rescind). Cryptographic key methods only remove the need for a repeat of the trust decision. After the first exchange, trust is no longer needed, because the keys allow identity to be actually verified.
Even if you leave the trust options switched on, you are not blindly trusting
the hosts you know about. The only potential insecurity lies in any new keys
that you have not thought about. If you use wildcards or IP prefixes in the
trust rules, then other hosts might be able to spoof their way in on trust
because you have left open a hole for them to exploit. That is why it is
recommended to set the system to the state of zero trust
immediately after key transfer, by commenting or emptying out the trust options
(trustkeysfrom
on the server).
It is possible, though somewhat laborious, to transfer the keys out of band,
by copying /var/cfengine/ppkeys/localhost.pub
to
/var/cfengine/ppkeys/user-aaa.bbb.ccc.mmm
(assuming IPv4) on another host.
e.g.
localhost.pub -> root-128.39.74.71.pub
Other users than root
CFEngine normally runs as user "root" (except on Windows which does not normally have a root user), i.e. a privileged administrator. If other users are to be granted access to the system, they must also generate a key and go through the same process. In addition, the users must be added to the server configuration file.
Encryption
CFEngine has 2 communication protocols. classic
or 1
and 2
or latest
.
Each protocol provides different encryption options for keeping file contents
private during transfer.
However, the main role of encryption in configuration management is for authentication. Secrets should not be transferred through policy, encrypted or not. Policy files should be considered public, and any leakage should not reveal secret information.
Note: Connections from the cf-agent are cached as described in the
documentation for body copy_from
.
Protocol Classic
Encryption for Enterprise is symmetric AES 256 bit in CBC mode, using a session key exchanged during the RSA handshake.
In core/community as outgoing outlined in the
body copy_from encrypt
documentation the initial
connection is encrypted using the public/private keys for the client
and server hosts. After the initial connection is established
subsequent connections and data transfer is encrypted by a randomly
generated Blowfish key that is refreshed each session.
With the classic protocol cf-serverd has the ability to enforce that a
file transfer be encrypted by setting the
ifencrypted
access attribute. When ACLs that
require encryption have unencrypted access attempts cf-serverd logs an
error message indicating the file requires encryption. Access to files
that cf-serverd requires to be encrypted can be logged by setting the
body server control logencryptedtransfers
attribute.
Protocol 2
3.6 introduced a new protocol option for communication with cf-serverd. Protocol 2 is the default in 3.7+ and uses a TLS session for encryption.
Note: When protocol 2 is in use legacy encryption attributes are noop.
The following attributes are affected:
encrypt
in copy from bodiesifencrypted
in in access promiseslogencryptedtransfers
in body common control
The specific encryption algorithm used depends on the cipher
negotiated between the client and the server. You can control which
ciphers are allowed by cf-serverd for incoming connections by
setting the
body server control allowciphers
attribute. Controlling
which ciphers are allowed to be used in outgoing connections is
done by setting
body common control tls_ciphers
.
Additionally the minimum version of TLS required for incoming
connections can be set in
body server control allowtlsversion
and the minimum version of TLS required for outgoing connections
can be set in
body common control tls_min_version
.
There are debug and verbose level logs produced by cf-agent to indicate when TLS is in use.
The following was captured by running the agent update policy in debug mode.
/var/cfenigne/bin/cf-agent -Kdf update.cf
verbose: Connected to host 192.168.33.2 address 192.168.33.2 port 5308
debug: TLSVerifyCallback: no ssl->peer_cert
debug: TLSVerifyCallback: no conn_info->key
debug: This must be the initial TLS handshake, accepting
verbose: TLS version negotiated: TLSv1.2; Cipher: AES256-GCM-SHA384,TLSv1/SSLv3
verbose: TLS session established, checking trust...
verbose: Received public key compares equal to the one we have stored
verbose: Server is TRUSTED, received key 'SHA=5d20c01e4230aa53863eb36686eaa882094cdbddf53545616dfd588f00cc0659' MATCHES stored one.
debug: TLSRecvLines(): CFE_v2 cf-serverd 3.7.1.
debug: TLSRecvLines(): OK WELCOME USERNAME=root
cf-serverd emits verbose and debug log messages indicating when TLS is in use.
The following was captured by starting cf-serverd in the foreground with debug mode.
/var/cfenigne/bin/cf-serverd -Fd
verbose: New connection (from 192.168.33.3, sd 7), spawning new thread...
verbose: CollectCallHasPending: false
debug: Waiting at incoming select...
info: 192.168.33.3> Accepting connection
verbose: 192.168.33.3> Setting socket timeout to 600 seconds.
verbose: 192.168.33.3> Peeked nothing important in TCP stream, considering the protocol as TLS
debug: 192.168.33.3> Peeked data: ....2......ak.
debug: 192.168.33.3> TLSVerifyCallback: no ssl->peer_cert
debug: 192.168.33.3> TLSVerifyCallback: no conn_info->key
debug: 192.168.33.3> This must be the initial TLS handshake, accepting
verbose: 192.168.33.3> TLS version negotiated: TLSv1.2; Cipher: AES256-GCM-SHA384,TLSv1/SSLv3
verbose: 192.168.33.3> TLS session established, checking trust...
debug: 192.168.33.3> TLSRecvLines(): CFE_v2 cf-agent 3.7.1.
debug: 192.168.33.3> TLSRecvLines(): IDENTITY USERNAME=root.
verbose: 192.168.33.3> Setting IDENTITY: USERNAME=root
verbose: 192.168.33.3> Received public key compares equal to the one we have stored
verbose: 192.168.33.3> SHA=4f25279831eeaf579d2e3451345854a93fdefc856ad741bd59515b859fb84dea: Client is TRUSTED, public key MATCHES stored one.
Troubleshooting
When setting up cf-serverd
, you might see the error message
Unspecified server refusal
This means that cf-serverd
is unable or is unwilling to authenticate the
connection from your client machine. The message is deliberately non-specific
so that anyone attempting to attack or exploit the service will not be given
information which might be useful to them.
There is a simple checklist for curing this problem:
- Make sure that you have granted access to the client's address in the
server control
body. - Make sure the connecting client is granted access to the requested resources
(files usually) in the
access_rules
promise bundle. - See the verbose log of the server for the exact error message, since the
client always gets the "Unspecified server refusal" reply from the server.
To run the server in verbose, kill cf-serverd on the policy hub and run:
$ cf-serverd -v
and then manually run
cf-agent
on the client. - In the unlikely case that you still get no indication of the denial, try
increasing the agent run verbosity.
cf-agent -I
for info-level messages or evencf-agent -v
for verbose.