NETCONF Call Home
-----------------
-.. important::
-
- The call home feature is experimental and will change in a future
- release. In particular, the Yang models will change to those specified
- in the `RFC 8071 <https://tools.ietf.org/html/rfc8071>`__
-
Call Home Installation
~~~~~~~~~~~~~~~~~~~~~~
.. note::
- In order to test Call Home functionality we recommend Netopeer.
- See `Netopeer Call Home <https://github.com/CESNET/netopeer/wiki/CallHome>`__ to learn how to enable call-home on Netopeer.
+ In order to test Call Home functionality we recommend Netopeer or
+ Netopeer2. See `Netopeer Call Home <https://github.com/CESNET/netopeer/wiki/CallHome>`__
+ or `Netopeer2 <https://github.com/CESNET/netopeer2>`__ to learn how to
+ enable call-home on Netopeer.
Northbound Call-Home API
~~~~~~~~~~~~~~~~~~~~~~~~
Global Configuration
^^^^^^^^^^^^^^^^^^^^
+.. important::
+ The global configuration is not a part of the `RFC 8071
+ <https://tools.ietf.org/html/rfc8071>`__ and, therefore, subject to change.
+
Configuring global credentials
''''''''''''''''''''''''''''''
-ODL Call-Home server allows user to configure global credentials, which
-will be used for devices which does not have device-specific credentials
-configured.
+ODL Call-Home server allows user to configure global credentials, which will be
+used for connected over SSH transport protocol devices which does not have
+device-specific credentials configured.
This is done by creating
``/odl-netconf-callhome-server:netconf-callhome-server/global/credentials``
*Configuring global username & passwords to try*
-.. code-block:: none
+.. code-block::
PUT
/restconf/config/odl-netconf-callhome-server:netconf-callhome-server/global/credentials HTTP/1.1
security issue, this also causes the Call-Home Server to drastically increase its output
to the log.
-.. code-block:: none
+.. code-block::
POST
/restconf/config/odl-netconf-callhome-server:netconf-callhome-server/global HTTP/1.1
Device-Specific Configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Allowing Device & Configuring Name
-''''''''''''''''''''''''''''''''''
+Netconf Call Home server supports both of the secure transports used
+by the Network Configuration Protocol (NETCONF) - Secure Shell (SSH),
+and Transport Layer Security (TLS).
+
+Configure device to connect over SSH protocol
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Netconf Call Home Server uses device provided SSH server key (host key)
to identify device. The pairing of name and server key is configured in
immediately. In either case, the device that connects to the Call home server
leaves a record of its presence in the operational store.
+Configuring Device with Device-specific Credentials
+'''''''''''''''''''''''''''''''''''''''''''''''''''
+
+Adding specific device to the allowed list is done by creating
+``/odl-netconf-callhome-server:netconf-callhome-server/allowed-devices/device/{device}``
+with device-id and connection parameters inside the ssh-client-params container.
+
+*Configuring Device with Credentials*
+
+.. code-block::
+
+ PUT
+ /restconf/config/odl-netconf-callhome-server:netconf-callhome-server/allowed-devices/device/example HTTP/1.1
+ Content-Type: application/json
+ Accept: application/json
+
+.. code-block:: json
+
+ {
+ "device": {
+ "unique-id": "example",
+ "ssh-client-params": {
+ "credentials": {
+ "username": "example",
+ "passwords": [ "password" ]
+ },
+ "ssh-host-key": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDHoH1jMjltOJnCt999uaSfc48ySutaD3ISJ9fSECe1Spdq9o9mxj0kBTTTq+2V8hPspuW75DNgN+V/rgJeoUewWwCAasRx9X4eTcRrJrwOQKzb5Fk+UKgQmenZ5uhLAefi2qXX/agFCtZi99vw+jHXZStfHm9TZCAf2zi+HIBzoVksSNJD0VvPo66EAvLn5qKWQD4AdpQQbKqXRf5/W8diPySbYdvOP2/7HFhDukW8yV/7ZtcywFUIu3gdXsrzwMnTqnATSLPPuckoi0V2jd8dQvEcu1DY+rRqmqu0tEkFBurlRZDf1yhNzq5xWY3OXcjgDGN+RxwuWQK3cRimcosH"
+ }
+ }
+ }
+
+Configuring Device with Global Credentials
+'''''''''''''''''''''''''''''''''''''''''''''''''''
+
+It is possible to omit 'username' and 'password' for ssh-client-params,
+in such case values from global credentials will be used.
+
*Example of configuring device*
-.. code-block:: none
+.. code-block::
+
+ PUT
+ /restconf/config/odl-netconf-callhome-server:netconf-callhome-server/allowed-devices/device/example HTTP/1.1
+ Content-Type: application/json
+ Accept: application/json
+
+.. code-block:: json
+
+ {
+ "device": {
+ "unique-id": "example",
+ "ssh-client-params": {
+ "host-key": "AAAAB3NzaC1yc2EAAAADAQABAAABAQDHoH1jMjltOJnCt999uaSfc48ySutaD3ISJ9fSECe1Spdq9o9mxj0kBTTTq+2V8hPspuW75DNgN+V/rgJeoUewWwCAasRx9X4eTcRrJrwOQKzb5Fk+UKgQmenZ5uhLAefi2qXX/agFCtZi99vw+jHXZStfHm9TZCAf2zi+HIBzoVksSNJD0VvPo66EAvLn5qKWQD4AdpQQbKqXRf5/W8diPySbYdvOP2/7HFhDukW8yV/7ZtcywFUIu3gdXsrzwMnTqnATSLPPuckoi0V2jd8dQvEcu1DY+rRqmqu0tEkFBurlRZDf1yhNzq5xWY3OXcjgDGN+RxwuWQK3cRimcosH"
+ }
+ }
+ }
+
+Deprecated configuration models for devices accessed with SSH protocol
+''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
+
+With `RFC 8071 <https://tools.ietf.org/html/rfc8071>`__ alignment and adding
+support for TLS transport following configuration models has been marked
+deprecated.
+
+Configuring Device with Global Credentials
+'''''''''''''''''''''''''''''''''''''''''''''''''''
+
+*Example of configuring device*
+
+.. code-block::
PUT
/restconf/config/odl-netconf-callhome-server:netconf-callhome-server/allowed-devices/device/example HTTP/1.1
*Configuring Device with Credentials*
-.. code-block:: none
+.. code-block::
PUT
/restconf/config/odl-netconf-callhome-server:netconf-callhome-server/allowed-devices/device/example HTTP/1.1
}
}
+Configure device to connect over TLS protocol
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Netconf Call Home Server allows devices to use TLS transport protocol to
+establish a connection towards the NETCONF device. This communication
+requires proper setup to make two-way TLS authentication possible for client
+and server.
+
+The initial step is to configure certificates and keys for two-way TLS by
+storing them within the netconf-keystore.
+
+*Adding a client private key credential to the netconf-keystore*
+
+.. code-block::
+
+ POST
+ /rests/operations/netconf-keystore:add-keystore-entry HTTP/1.1
+ Content-Type: application/json
+ Accept: application/json
+
+.. code-block:: json
+
+ {
+ "input": {
+ "key-credential": [
+ {
+ "key-id": "example-client-key-id",
+ "private-key": "base64encoded-private-key",
+ "passphrase": "passphrase"
+ }
+ ]
+ }
+ }
+
+*Associate a private key with a client and CA certificates chain*
+
+.. code-block::
+
+ POST
+ /rests/operations/netconf-keystore:add-private-key HTTP/1.1
+ Content-Type: application/json
+ Accept: application/json
+
+.. code-block:: json
+
+ {
+ "input": {
+ "private-key": [
+ {
+ "name": "example-client-key-id",
+ "data": "key-data",
+ "certificate-chain": [
+ "certificate-data"
+ ]
+ }
+ ]
+ }
+ }
+
+*Add a list of trusted CA and server certificates*
+
+.. code-block::
+
+ POST
+ /rests/operations/netconf-keystore:add-trusted-certificate HTTP/1.1
+ Content-Type: application/json
+ Accept: application/json
+
+.. code-block:: json
+
+ {
+ "input": {
+ "trusted-certificate": [
+ {
+ "name": "example-ca-certificate",
+ "certificate": "ca-certificate-data"
+ },
+ {
+ "name": "example-server-certificate",
+ "certificate": "server-certificate-data"
+ }
+ ]
+ }
+ }
+
+In a second step, it is required to create an allowed device associated with
+a server certificate and client key. The server certificate will be used to
+identify and pin the NETCONF device during SSL handshake and should be unique
+among the allowed devices.
+
+*Add device configuration for TLS protocol to allowed devices list*
+
+.. code-block::
+
+ PUT
+ /restconf/config/odl-netconf-callhome-server:netconf-callhome-server/allowed-devices/device/example-device HTTP/1.1
+ Content-Type: application/json
+ Accept: application/json
+
+.. code-block:: json
+
+ {
+ "device": {
+ "unique-id": "example-device",
+ "tls-client-params": {
+ "key-id": "example-client-key-id",
+ "certificate-id": "example-server-certificate"
+ }
+ }
+ }
+
Operational Status
^^^^^^^^^^^^^^^^^^
type string;
}
leaf ssh-host-key {
- description "BASE-64 encoded public key which device will use during connection.";
+ description "BASE-64 encoded public key which device will use during connection.
+ Deprecated, a 'host-key' from the 'ssh-client-params' containers should be used instead.";
+ status deprecated;
type string;
}
unique ssh-host-key;
uses credentials;
+
+ choice transport {
+ description "Provides connectivity details for one of the supported transport protocols";
+ case ssh {
+ container ssh-client-params {
+ leaf host-key {
+ mandatory true;
+ description "BASE-64 encoded public key which device will use during connection.";
+ type string;
+ }
+ uses credentials;
+ }
+ }
+ case tls {
+ container tls-client-params {
+ leaf certificate-id {
+ mandatory true;
+ description "Certificate identifier which will be used during two-way TLS authentication.";
+ type string;
+ }
+ leaf key-id {
+ mandatory true;
+ description "Key identifier inside the NetConf keystore which will be used during two-way TLS authentication.";
+ type string;
+ }
+ }
+ }
+ }
+ unique certificate-id;
+ unique host-key;
}
}
}
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.Global;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.Global.MountPointNamingStrategy;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.Device;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.Ssh;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.ssh.SshClientParams;
import org.opendaylight.yangtools.concepts.ListenerRegistration;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
import org.slf4j.Logger;
if (deviceSpecific != null) {
sessionName = deviceSpecific.getUniqueId();
- deviceCred = deviceSpecific.getCredentials();
+ if (deviceSpecific.getTransport() instanceof Ssh) {
+ final SshClientParams clientParams = ((Ssh) deviceSpecific.getTransport()).getSshClientParams();
+ deviceCred = clientParams.getCredentials();
+ } else {
+ deviceCred = deviceSpecific.getCredentials();
+ }
} else {
String syntheticId = fromRemoteAddress(remoteAddress);
if (globalConfig.allowedUnknownKeys()) {
private void deleteDevice(final Device dataBefore) {
if (dataBefore != null) {
- final String publicKey = dataBefore.getSshHostKey();
+ final String publicKey = getHostPublicKey(dataBefore);
if (publicKey != null) {
LOG.debug("Removing device {}", dataBefore.getUniqueId());
removeDevice(publicKey, dataBefore);
}
private void writeDevice(final Device dataAfter) {
- final String publicKey = dataAfter.getSshHostKey();
+ final String publicKey = getHostPublicKey(dataAfter);
if (publicKey != null) {
LOG.debug("Adding device {}", dataAfter.getUniqueId());
addDevice(publicKey, dataAfter);
}
}
+ private String getHostPublicKey(final Device device) {
+ if (device.getTransport() instanceof Ssh) {
+ return ((Ssh) device.getTransport()).getSshClientParams().getHostKey();
+ } else {
+ return device.getSshHostKey();
+ }
+ }
+
abstract void addDevice(String publicKey, Device device);
abstract void removeDevice(String publicKey, Device device);
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.Device;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceBuilder;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceKey;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.Transport;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.Ssh;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.SshBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.ssh.SshClientParams;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.ssh.SshClientParamsBuilder;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.TopologyId;
@Override
public void onDataTreeChanged(final Collection<DataTreeModification<Node>> changes) {
- for (DataTreeModification<Node> change: changes) {
+ for (DataTreeModification<Node> change : changes) {
final DataObjectModification<Node> rootNode = change.getRootNode();
final InstanceIdentifier<Node> identifier = change.getRootPath().getRootIdentifier();
switch (rootNode.getModificationType()) {
}
private static Device newDevice(final String id, final PublicKey serverKey, final Device1.DeviceStatus status) {
+ // used only for netconf devices that are connected via SSH transport and global credentials
String sshEncodedKey = serverKey.toString();
try {
sshEncodedKey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
} catch (IOException e) {
LOG.warn("Unable to encode public key to ssh format.", e);
}
+ final SshClientParams sshParams = new SshClientParamsBuilder().setHostKey(sshEncodedKey).build();
+ final Transport transport = new SshBuilder().setSshClientParams(sshParams).build();
return new DeviceBuilder()
.setUniqueId(id)
.withKey(new DeviceKey(id))
- .setSshHostKey(sshEncodedKey)
- .addAugmentation(new Device1Builder().setDeviceStatus(Device1.DeviceStatus.FAILEDNOTALLOWED).build())
+ .setTransport(transport)
+ .addAugmentation(new Device1Builder().setDeviceStatus(status).build())
.build();
}
private static InstanceIdentifier<Device> buildDeviceInstanceIdentifier(final NodeId nodeId) {
return InstanceIdentifier.create(NetconfCallhomeServer.class)
- .child(AllowedDevices.class)
- .child(Device.class, new DeviceKey(nodeId.getValue()));
+ .child(AllowedDevices.class)
+ .child(Device.class, new DeviceKey(nodeId.getValue()));
}
private static Device withConnectedStatus(final Device opDev) {
}
private static Device deviceWithStatus(final Device opDev, final DeviceStatus status) {
- return new DeviceBuilder()
- .setUniqueId(opDev.getUniqueId())
- .setSshHostKey(opDev.getSshHostKey())
- .addAugmentation(new Device1Builder().setDeviceStatus(status).build())
- .build();
+ final DeviceBuilder deviceBuilder = new DeviceBuilder()
+ .setUniqueId(opDev.getUniqueId())
+ .addAugmentation(new Device1Builder().setDeviceStatus(status).build());
+ if (opDev.getTransport() != null) {
+ deviceBuilder.setTransport(opDev.getTransport());
+ } else {
+ deviceBuilder.setSshHostKey(opDev.getSshHostKey());
+ }
+ return deviceBuilder.build();
}
private void setDeviceStatus(final Device device) {
public void reportFailedAuth(final PublicKey sshKey) {
AuthorizedKeysDecoder decoder = new AuthorizedKeysDecoder();
- for (Device device : getDevicesAsList()) {
- String keyString = device.getSshHostKey();
+ for (final Device device : getDevicesAsList()) {
+ final String keyString;
+ if (device.getTransport() instanceof Ssh) {
+ keyString = ((Ssh) device.getTransport()).getSshClientParams().getHostKey();
+ } else {
+ keyString = device.getSshHostKey();
+ }
if (keyString == null) {
LOG.info("Whitelist device {} does not have a host key, skipping it", device.getUniqueId());
continue;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.Device;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceBuilder;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceKey;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.Transport;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.Ssh;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.SshBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.ssh.SshClientParams;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.device.transport.ssh.SshClientParamsBuilder;
import org.opendaylight.yangtools.concepts.ListenerRegistration;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
import org.slf4j.Logger;
devStatus = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.DISCONNECTED).build();
}
- tx.merge(LogicalDatastoreType.OPERATIONAL, deviceIID, new DeviceBuilder()
- .addAugmentation(devStatus).setSshHostKey(cfgDevice.getSshHostKey())
- .setUniqueId(cfgDevice.getUniqueId()).build());
+ final Device opDevice = createOperationalDevice(cfgDevice, devStatus);
+ tx.merge(LogicalDatastoreType.OPERATIONAL, deviceIID, opDevice);
tx.commit().addCallback(new FutureCallback<CommitInfo>() {
@Override
public void onSuccess(final CommitInfo result) {
}
}, MoreExecutors.directExecutor());
}
+
+ private Device createOperationalDevice(final Device cfgDevice, final Device1 devStatus) {
+ final DeviceBuilder deviceBuilder = new DeviceBuilder()
+ .addAugmentation(devStatus)
+ .setUniqueId(cfgDevice.getUniqueId());
+ if (cfgDevice.getTransport() instanceof Ssh) {
+ final String hostKey = ((Ssh) cfgDevice.getTransport()).getSshClientParams().getHostKey();
+ final SshClientParams params = new SshClientParamsBuilder().setHostKey(hostKey).build();
+ final Transport sshTransport = new SshBuilder().setSshClientParams(params).build();
+ deviceBuilder.setTransport(sshTransport);
+ } else if (cfgDevice.getSshHostKey() != null) {
+ deviceBuilder.setSshHostKey(cfgDevice.getSshHostKey());
+ }
+ return deviceBuilder.build();
+ }
}