Securing OpenDaylight’s REST Interfaces

OpenDaylight comes with a set of REST interfaces. For instance, in one of my previous posts, I have introduced OpenDaylight’s REST interface for programming flows. With these interfaces, you can easily outsource your control logic to a remote server other than the server on which the OpenDaylight controller is running. Basically, the controller offers a web service, and the control application invokes this service sending REST requests via HTTP.

Although the concept to offer network services as web services is very nice and lowers the barriers to “program” the network significantly, it also brings up security problems well known from web services. If you do not authenticate clients, any client that can send HTTP requests to your controller can control your network — certainly something you want to avoid!

Therefore, in this post, I will show how to secure OpenDaylight’s REST interfaces.

Authentication in OpenDaylight

The REST interfaces are so-called northbound interfaces between controller and control application. So you can think of the controller as the service and the control application as the client as shown in the figure below.

insecure_network

In order to ensure that the controller only accepts requests from authorized clients, clients have to authenticate themselves. OpenDaylight uses HTTP Basic authentication, which is based on user names and passwords (default: admin, admin). Sounds good: So only a client with the valid password can invoke the service … or is there a problem? In order to see the security threats, we have to take a closer look at the HTTP Basic authentication mechanism.

The follwing command invokes the Flow Programmer service of OpenDaylight via cURL and prints the HTTP header information of the request:

user@host:$ curl -u admin:admin -H 'Accept: application/xml' -v 'http://localhost:8080/controller/nb/v2/flowprogrammer/default/'
* About to connect() to localhost port 8080 (#0)
* Trying 127.0.0.1... connected
* Server auth using Basic with user 'admin'
> GET /controller/nb/v2/flowprogrammer/default/ HTTP/1.1
> Authorization: Basic YWRtaW46YWRtaW4=
> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: localhost:8080
> Accept: application/xml
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Cache-Control: private
< Expires: Thu, 01 Jan 1970 01:00:00 CET
< Set-Cookie: JSESSIONIDSSO=9426E0F12A0A0C80BE549451707EF339; Path=/
< Set-Cookie: JSESSIONID=DB23D1EE61348E101E6CE8117A04B8D8; Path=/
< Content-Type: application/xml
< Content-Length: 62
< Date: Sun, 12 Jan 2014 16:50:38 GMT
<
* Connection #0 to host localhost left intact
* Closing connection #0
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><list/>

The interesting header field is “Authorization” with its value “Basic YWRtaW46YWRtaW4=”. Here, “YWRtaW46YWRtaW4=” is the user name and password sent from the client to the controller. Although this value looks quite cryptic, it is actually plain text. This value is simply the Base64 encoding of the user name and password string “admin:admin”. Base64 is a simple translation of 8 bit characters to 6 bit characters involving no encryption or hashing at all! Basically, it comes from the time when SMTP was restricted to sending 7 bit ASCII characters. Everything else like binary (8 bit) content had to be translated to 7 bit characters first, and exactly that’s the job of Base64 encoding. You can use a paper and pencil to decode it. Just interpret the bit pattern of three 8 bit characters as four 6 bit characters and look-up the values of the 6 bit characters in the Base64 table. Or if you are lazy, just use one of the many Base64 decoders in the WWW.

Now the problem should become obvious. If your network between client and controller is non-trusted and an attacker can eavesdrop on the communication channel, he can read your user name and password.

Securing the REST Interface

Now that we see the problem, also the solution should become obvious. We need a secure channel between client and controller, so an attacker cannot read the header fields of the HTTP request. The HTTPS standard provides exactly that. Moreover, the client can make sure that it really connects to the right controller, and not the controller of an attacker who just wants to intercept our password. So we use HTTPS to encrypt the channel between client and controller and to authenticate the controller, and HTTP Basic authentication to authenticate the client.

So the trick is, enabling HTTPS in OpenDaylight, which is turned off by default. Note that above we used the insecure HTTP protocol on port 8080. Now we want to use HTTPS on port 8443 (or 443 if you want to use the official HTTPS port instead of the alternative port).

OpenDaylight uses the Tomcat servlet container to provide its web services. Therefore, the steps to enable HTTPS are very similar to configuring Tomcat.

First, we need a server certificate that the client can use to authenticate the server. Of course, a certificate signed by a trusted certification authority would be best. However, here I will show how to create your own self-signed certificate using the Java keytool:

user@host:$ keytool -genkeypair -v -alias tomcat -storepass changeit -validity 1825 -keysize 1024 -keyalg DSA
What is your first and last name?
[Unknown]: duerr-mininet.informatik.uni-stuttgart.de
What is the name of your organizational unit?
[Unknown]: Institute of Parallel and Distributed Systems
What is the name of your organization?
[Unknown]: University of Stuttgart
What is the name of your City or Locality?
[Unknown]: Stuttgart
What is the name of your State or Province?
[Unknown]: Baden-Wuerttemberg
What is the two-letter country code for this unit?
[Unknown]: de
Is CN=duerr-mininet.informatik.uni-stuttgart.de, OU=Institute of Parallel and Distributed Systems, O=University of Stuttgart, L=Stuttgart, ST=Baden-Wuerttemberg, C=de correct?
[no]: yes

Generating 1,024 bit DSA key pair and self-signed certificate (SHA1withDSA) with a validity of 1,825 days
for: CN=duerr-mininet.informatik.uni-stuttgart.de, OU=Institute of Parallel and Distributed Systems, O=University of Stuttgart, L=Stuttgart, ST=Baden-Wuerttemberg, C=de
Enter key password for <tomcat>
(RETURN if same as keystore password):
Re-enter new password:
[Storing /home/duerrfk/.keystore]

This creates a certificate valid for five years (1825 days) and stores it in the keystore .keystore in my home directory /home/duerrfk. As first and last name, we use the DNS name of the machine, the controller is running on. The rest should be pretty obvious.

With this information, we can now configure the OpenDaylight controller. First, check out and compile OpenDaylight, if you haven’t done so already:

user@host:$ git clone https://git.opendaylight.org/gerrit/p/controller.git
user@host:$ cd controller/opendaylight/distribution/opendaylight/
user@host:$ mvn clean install

Now edit the following file, where “controller” is the root directory of the controller you checked out:

user@host:$ emacs controller/opendaylight/distribution/opendaylight/target/distribution.opendaylight-osgipackage/opendaylight/configuration/tomcat-server.xml

Comment in the following XML element:

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
maxThreads="150" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS"
keystoreFile="/home/duerrfk/.keystore"
keystorePass="changeit"/>

Use the keystore location and password that you used before with the keytool command.

Now you can start OpenDaylight and connect via HTTPS to the controller on port 8443. Use your web browser to try it.

Making Secure Calls

One last step needs to be done before we can call the controller securely by a client. If you did not use a certificate signed by a well-known certification authority — like we did above with our self-signed certficate –, you need to present the client with the server certificate it should use for authenticating the controller. If you are using cURL, the required option is “–cacert”:

user@host:$ curl -u admin:admin --cacert ~/cert-duerr-mininet.pem -v -H 'Accept: application/xml' 'https://duerr-mininet.informatik.uni-stuttgart.de:8443/controller/nb/v2/flowprogrammer/default/'

So the last question is, how do we get the server certificate in PEM format? To this end, we can use openssl to call our server and store the returned certificate:

user@host:$ openssl s_client -connect duerr-mininet.informatik.uni-stuttgart.de:8443

The PEM certificate is everything between “—–BEGIN CERTIFICATE—–” and “—–END CERTIFICATE—–” (including these two lines), so we can just copy this to a file. Note that you have to make sure that the call is actually going to the right server (not the server of an attacker). So better call it from the machine where your controller is running to avoid a “chicken and egg” problem.

Summary

Now we can securely outsource our control application to a remote host, for instance, a host in our campus network or a cloud server running in a remote data center.

OpenDaylight: Programming Flows with the REST Interface and cURL

The SDN controller OpenDaylight comes with a Flow Programmer service, which makes it very easy for applications to program flows by using a REST interface. In this post, I will show how to use this service together with the command line tool cURL to add, modify, and delete flows.

What is so nice about REST?

REST is based on technologies that many programmers already know, in particular, HTTP to transport requests and responses between client (control application) and service (e.g., OpenDaylight’s Flow Programmer), and XML and JSON to describe the parameters of a request and the result. Because of its simplicity (compared to other web service standads like SOAP), REST is very popular and used by many web services in the World Wide Web, e.g., by Twitter or Flickr.

Since REST is based on popular technologies like HTTP, JSON, and XML, you can use it in a more or less straightforward way with most programming languages like Java, Python, C (you name it), and even command line tools like cURL. Therefore, the barrier to use it is very low. I think, this very nicely shows how software-defined networking reduces the gap between application (programmer) and network. I am sure, after you have read this post, you will agree.

Having said this, it is also important to understand the limitations of REST in the scope of SDN. REST is based on request/response interaction, i.e., the control application (client) makes a request, and the service executes the request and returns the response. REST is less suited for event-based interaction, where the controller calls the application (strictly speaking, then the controller would be the client and the application the service since client and server are only roles of two communicating entities).

In OpenFlow, there is one important event that signals to the control application that a packet without matching flow table entry has arrived at a switch (packet-in event). In this case, a controll application implementing the control logic has to decide what to do: dropping the packet, forwarding it, or setting up a flow table entry for similar packets. Because of this limitation, the REST interface is limited to proactive flow programming, where the control application proactively programs the flow table of the switches; reactive flow programming where the control application reacts on packet-in events is implemented in OpenDaylight using OSGI components.

Programming Flows with REST

REST is all about resources and their states. REST stands for Representational State Transfer. What does that mean in the context of OpenDaylight’s Flow Programmer service? For the Flow Programmer service, you can think of resources as the whole network, a switch, or a flow. The basic job of the Flow Programmer is to query and change the state of these resources by returning, adding, or deleting flows. The state of a resource can be represented in different formats, namely, XML or JSON.

Adding Flows

That was still very abstract, wasn’t it? According to Einstein, the only way to explain something is by example. So let’s make some examples. First of all, we will add a flow to a switch. We use a very simple linear topology with two switches and two hosts depicted in the following figure.

mininet-topology

You can create this topology in Mininet with the following command assuming the OpenDaylight controller is running on the machine with IP 192.168.1.1:

mn --controller=remote,ip=192.168.1.1 --topo linear,2

We also open X-terminals for both hosts using the following commands:

mininet> xterm h1
mininet> xterm h2

With the command ifconfig in the respective terminal, you can find out the IP addresses of the hosts (h1: 10.0.0.1; h2: 10.0.0.2).

OpenDaylight already implements reactive forwarding logic, so a ping between host h1 and host h2 already works! You can send a ping from h1 to h2 to check this:

h1> ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_req=1 ttl=64 time=0.368 ms
64 bytes from 10.0.0.2: icmp_req=2 ttl=64 time=0.050 ms
64 bytes from 10.0.0.2: icmp_req=3 ttl=64 time=0.059 ms
64 bytes from 10.0.0.2: icmp_req=4 ttl=64 time=0.058 ms

Since these flows were programmed reactively using an OSGI module, we cannot see them in the Flow Programmer service! This service only knows about the flows that were created via the Flow Programmer itself.

Now let’s program a flow that blocks all TCP requests to port 80 (web server) targeted at Host 2. You can think of this as an in-network firewall. Instead of blocking the traffic at the host using a software firewall, we already block it on a switch, e.g., at a top-of-rack switch of the rack where h2 is located or even earlier at the core switches of your data center to keep your network free from unwanted traffic.

Before we setup our blocking flow entry, we verify that h1 can send requests on port 80 to h2 using netcat to simulate a web server and client:

h2> nc -lk 10.0.0.2 80
Hello
h1> echo "Hello" | nc 10.0.0.2 80

Here, h2 is listening (option -l) on port 80 for incoming requests (and stays listening; option -k). h1 sends the String “Hello”, and h2 displays this string showing that the connection works.

I will now show you the cURL command to block TCP datagrams to port 80 at switch s2, and then explain the details:

controller> curl -u admin:admin -H 'Content-type: application/json' -X PUT -d '{"installInHw":"true", "name":"flow1", "node": {"id":"00:00:00:00:00:00:00:02", "type":"OF"}, "ingressPort":"2", "etherType": "0x800", "protocol": "6", "tpDst": "80", "priority":"65535","actions":["DROP"]}' 'http://localhost:8080/controller/nb/v2/flowprogrammer/default/node/OF/00:00:00:00:00:00:00:02/staticFlow/flow1'

We are using option “-u” to specify the user name and password (both “admin” which you might want to change for obvious reasons). OpenDaylight uses HTTP Basic authentication, so every request to a northbound REST interface must be authenticated with a user name and password.

According to the REST paradigm, resources are added using HTTP PUT requests. Therefore, we set the cURL option “-X PUT” to add the flow. If a resource with the same id already exists, it will be modified.

Moreover, we specify the HTTP content type as “application/json” since we are sending our request as JSON representation. You could as well use XML (Content-type: application/XML), however, here we use the simpler JSON format.

With option “-d”, we set the payload of the HTTP request, which in our case is a JSON document defining the flow to be added. A JSON document consists of a number of key value pairs separated by colon, so it should be very easy to read the above example. Our new flow gets the name “flow1”, so we can later refer to it to modify or delete it. It will be installed in hardware on the switch — whatever that means for our emulated mininet 😉 The value of the key “node” consists of a JSON structure (marked by “{..}” in JSON) with the keys “id” and “type”. These are the data path id and type (OF = OpenFlow) of the switch. Actually, adding node id, type, and flow name is redundant from my point of view, because it is also included in the URL as you can see. According to the REST paradigm, the URL is the right place to specify the path of the resource. Anyway, it will not work without, so we better include it.

The rest defines the values of our flow. In order to match datagrams to port 80 over TCP/IP, you have to specify (1) the ethertype “0x800” to identify IPv4 packets; (2) protocol id 6 to identify TCP datagrams; (3) the transport layer destination address (also known as port) 80. Moreover, we specify the incoming port and a flow priority. Priority 65535 is the highest priority, so we are sure that this entry will be effective if there are other entries that match the same packet (e.g., setup by the reactive forwarding module of OpenDaylight). Finally, we have to specify an action. In this case, the packet will be dropped. Another frequently used action is to output the packet on a certain port (e.g., “OUTPUT=1”). As you can see, the action field is an array (marked by “[..]” in JSON), so you can specify multiple actions.

So let’s see whether our flow table entry has some effect by sending another hello message via TCP/IP to port 80:

h1> echo "Hello" | nc 10.0.0.2 80
h2> echo $?
1

The second command returns the exit status of the first command, which is 1, i.e, non-zero, so it failed. Therefore, no packets to port 80 arrived at h2 anymore. You can check whether other ports are still accessible by changing the port number of the netcat commands. Actually, they are, so everything worked as expected and we successfully programmed our first flow.

Querying Flows

Now that we have programmed a flow, we can query for installed flows. According to the REST paradigm, you can query the state of a resource using a GET request. The specific request for querying all flows of the network looks as follows:

controller> curl -u admin:admin -H 'Accept: application/xml' 'http://localhost:8080/controller/nb/v2/flowprogrammer/default'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><list><flowConfig><installInHw>true</installInHw><name>flow1</name><node><id>00:00:00:00:00:00:00:02</id><type>OF</type></node><ingressPort>2</ingressPort><priority>65535</priority><etherType>0x800</etherType><protocol>6</protocol><tpDst>80</tpDst><actions>DROP</actions></flowConfig></list>

Note that this time, we are using XML as accpeted content type by setting the HTTP header field “Accepted” to “application/xml”. We could as well use JSON again, which is easier to parse and smaller.

We can also query a particular switch by specifying its data path id in the URL:

curl -u admin:admin -H 'Accept: application/xml' 'http://localhost:8080/controller/nb/v2/flowprogrammer/default/node/OF/00:00:00:00:00:00:00:02'

Or we can query the definition of a certain flow using the flow’s name in the resource URL:

curl -u admin:admin -H 'Accept: application/xml' 'http://localhost:8080/controller/nb/v2/flowprogrammer/default/node/OF/00:00:00:00:00:00:00:02/staticFlow/flow1'

Deleting Flows

Finally, we can also delete flows using the HTTP DELETE request:

curl -u admin:admin -X DELETE 'http://localhost:8080/controller/nb/v2/flowprogrammer/default/node/OF/00:00:00:00:00:00:00:02/staticFlow/flow1'

The URL specifies the resource (here of a flow) to be deleted. HTTP DELETE requests have no payload — I am just mentioning this because the Floodlight controller required a payload for delete requests, which most HTTP implementations refuse to send; so the OpenDaylight interface is cleaner and more REST’ish in that respect.

Summary

That’s basically it. I hope, you agree that programming flows using REST is really simple. In future posts, I plan to introduce other northbound REST APIs of OpenDaylight and also the OSGI interface for reactive flow programming. If you are interested, I hope to see you again.

If you need further information about the Flow Programmer REST interface, you can visit the OpenDaylight website.