RESTCONF Service Setup with Certificate-Based Authentication and NACM

Use Case

RESTCONF is desirable for its ability to implement changes to TNSR remotely using the API, but allowing remote changes to TNSR also raises security concerns. When using RESTCONF, security is extremely important to protect the integrity of the router against unauthorized changes.

Note

RESTCONF deals in JSON output and input, which is easily parsed by a variety of existing libraries for programming and scripting languages.

Example Scenario

In this example, TNSR will be configured to allow access via RESTCONF, but the service will be protected in several key ways:

  • The RESTCONF service is configured for TLS to encrypt the transport

  • The RESTCONF service is configured to require a client certificate, which is validated against a private Certificate Authority known to TNSR

  • NACM determines if the certificate common-name (username) is allowed access to view or make changes via RESTCONF

  • The service will run in the host namespace so it is exposed to the management network only, and not to public networks.

Item

Value

TNSR Hostname

tnsr.example.com

RESTCONF Username

tnsr

NACM Group Name

admins

Additional User

anotheruser

Host Interface Address

198.51.100.2

TNSR Setup

Current versions of TNSR software setup a default RESTCONF configuration and a set of basic PKI certificates sufficient to run and access the RESTCONF daemon at boot time if one does not already exist. This is equivalent to running the shortcut command below (pki generate-restconf-certs) and adding a RESTCONF server setup in the host namespace.

This initial setup is good enough for basic RESTCONF purposes but can be customized and improved in several ways as described in the remainder of this recipe.

See also

Administrators who are satisfied with the default setup may want to skip ahead to Example Usage for usage examples or Adding More RESTCONF Users for information on how to allow additional users to access RESTCONF.

Generate Certificates

There are two ways to create the necessary PKI structure for the restconf server: Using the shortcut command or creating the PKI certificates manually.

Certificate Shortcut Command

The shortcut command generates a basic set of certificates for use with the RESTCONF service, but does not offer as much customization as generating the certificates manually.

tnsr(config)# pki generate-restconf-certs length 4096 subject-alt-names tnsr.example.com 198.51.100.2
Generated new Certificates (and missing Keys - RSA 4096 bits):
CA          cert/key : restconf-CA          (New RSA key)
server side cert/key : restconf             (New RSA key)
client side cert/key : restconf-client      (New RSA key)

If this is sufficient, proceed ahead to Setup NACM, or continue reading to learn how to create the certificates manually instead.

Manually Generate Certificates

The PKI structure can also be generated manually for more fine-grained control, such as using different CA and certificate names, non-default digest options, different lifetimes, and different CN/SAN values.

Create a self-signed Certificate Authority:

tnsr(config)# pki private-key restconf-CA generate
tnsr(config)# pki signing-request settings clear
tnsr(config)# pki signing-request set common-name restconf-CA
tnsr(config)# pki signing-request set digest sha512
tnsr(config)# pki signing-request restconf-CA generate
tnsr(config)# pki signing-request restconf-CA sign self purpose ca

Create a certificate for the user tnsr, signed by restconf-CA:

tnsr(config)# pki private-key restconf-client generate key-length 4096
tnsr(config)# pki signing-request settings clear
tnsr(config)# pki signing-request set common-name tnsr
tnsr(config)# pki signing-request set digest sha512
tnsr(config)# pki signing-request restconf-client generate
tnsr(config)# pki signing-request restconf-client sign ca-name restconf-CA days-valid 365 digest sha512 purpose client

Create a certificate for the RESTCONF service to use. The common-name should be the hostname of the TNSR router, which should also exist in DNS:

tnsr(config)# pki private-key restconf generate key-length 4096
tnsr(config)# pki signing-request settings clear
tnsr(config)# pki signing-request set common-name tnsr.example.com
tnsr(config)# pki signing-request set subject-alt-names add hostname tnsr.example.com
tnsr(config)# pki signing-request set subject-alt-names add ipv4-address 198.51.100.2
tnsr(config)# pki signing-request set digest sha512
tnsr(config)# pki signing-request restconf generate
tnsr(config)# pki signing-request restconf sign ca-name restconf-CA days-valid 365 digest sha512 purpose server

Setup NACM

Disable NACM while making changes, to avoid locking out the account making the changes:

tnsr(config)# nacm disable

Set default policies:

tnsr(config)# nacm exec-default deny
tnsr(config)# nacm read-default deny
tnsr(config)# nacm write-default deny

Setup an admin group containing the default users plus tnsr, which will match the common-name of the user certificate created above:

tnsr(config)# nacm group admin
tnsr(config-nacm-group)# member root
tnsr(config-nacm-group)# member tnsr
tnsr(config-nacm-group)# exit

Setup rules to permit any action by members of the admin group:

tnsr(config)# nacm rule-list admin-rules
tnsr(config-nacm-rule-list)# group admin
tnsr(config-nacm-rule-list)# rule permit-all
tnsr(config-nacm-rule)# module *
tnsr(config-nacm-rule)# access-operations *
tnsr(config-nacm-rule)# action permit
tnsr(config-nacm-rule)# exit
tnsr(config-nacm-rule-list)# exit

Enable NACM:

tnsr(config)# nacm enable
tnsr(config)# exit

Enable RESTCONF

Enable RESTCONF and configure it for TLS on port 443 with client certificate authentication:

tnsr(config)# restconf
tnsr(config-restconf)# global authentication-type client-certificate
tnsr(config-restconf)# global server-ca-cert-path restconf-CA
tnsr(config-restconf)# global server-certificate restconf
tnsr(config-restconf)# global server-key restconf
tnsr(config-restconf)# server host 198.51.100.2 443 true
tnsr(config-restconf)# enable true

Client Configuration

On TNSR, export the CA certificate, user certificate, and user certificate key. This can be done individually or by using a PKCS#12 archive export.

Exporting separate CA, client certificate, and client key files

When exporting these entries, place the resulting files in a secure place on a client system, in a directory with appropriate permissions, readable only by the user. Additionally, the private key file must only be readable by the user. For this example, the files will be placed in ~/tnsr/.

First, export the CA certificate. Copy and paste this into a local file, named tnsr-restconf-CA.crt:

tnsr# pki ca restconf-CA get
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----

Next, export the user certificate, copy and paste it and save in a local file named tnsr-restconf-client.crt:

tnsr# pki certificate restconf-client get
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----

Finally, export the user certificate private key, copy and paste it and save in a local file named tnsr-restconf-client.key. Remember to protect this file so it is only readable by this user:

tnsr# pki private-key restconf-client get
-----BEGIN PRIVATE KEY-----
[...]
-----END PRIVATE KEY-----

This example uses curl to access RESTCONF, so ensure it is installed and available on the client computer.

Exporting a PKCS#12 archive

As an alternative to using the certificate and key separately, it’s also possible to export a PKCS#12 archive (PKCS#12 Archives) which contains the CA, client certificate, and client key. cURL and other utilities can utilize this archive for client certificate authentication.

When exporting the PKCS#12 archive, place the resulting file in a secure place on a client system, in a directory with appropriate permissions, readable only by the user. The archive bundle must be password-protected, but it’s still a best practice to ensure only the user has access to read the archive since it contains private key data.

Warning

Read through PKCS#12 Archives thoroughly and be aware of client platform requirements when choosing the algorithms used for encryption and hashing the archive. This recipe assumes the client is capable of using strong encryption.

To export an archive for the restconf-client certificate and key signed by the CA restconf-CA, use the following command (all on one line):

tnsr# pki pkcs12 restconf-client generate export-password abc12345 ca-name restconf-CA
   key-pbe-algorithm AES-256-CBC certificate-pbe-algorithm AES-256-CBC mac-algorithm sha256

The command will create a password-protected archive using the given password (minimum 8 characters) and it will write the archive out to a file both in the TNSR PKI store and in the current directory of the TNSR CLI client (e.g. the tnsr user home directory, /home/tnsr/):

P12 restconf-client stored in /etc/pki/tls/tnsr/certs, copied to
./restconf-client-20231026152004.p12

Copy the .p12 file off TNSR using scp or another program capable of using SCP, such as FileZilla. Then copy the file to the client, changing its name if necessary. For example, to make it easier to use with cURL, rename it to restconf-client.p12

Example Usage

This simple example shows fetching the contents of an ACL from RESTCONF as well as adding a new ACL entry. There are numerous possibilities here, for more details see the REST API documentation.

In this example, there is an existing ACL named blockbadhosts. It contains several entries including a default allow rule with a sequence number of 5000.

These examples are all run from the client configured above.

Note

This is a simple demonstration using cURL and shell commands. This makes it easy to demonstrate how the service works, and how RESTCONF URLs are formed, but does not make for a good practical example.

In real-world cases these types of queries would be handled by a program or script that interacts with RESTCONF, manipulating data directly and a lot of the details will be handled by RESTCONF and JSON programming libraries.

cURL Differences for CA+Certificate files vs PKCS#12 Archive

The cURL syntax in the examples will vary depending on the client certificate export format.

For CA and certificate files exported separately, use this style:

$ curl -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  [...]

Replace the filenames as needed.

For a PKCS#12 archive, use the following style:

$ curl -f --cert-type P12 --cert ~/tnsr/restconf-client.p12:'abc12345' \
  [...]

Replace the filename and password as needed.

The examples below use the separate file style syntax, but both methods work.

Retrive a specific ACL

Retrieve the entire contents of the blockbadhosts ACL:

Command:

$ curl -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  -X GET \
  https://tnsr.example.com/restconf/data/netgate-acl:acl-config/acl-table/acl-list=blockbadhosts

Output:

{
    "netgate-acl:acl-list": [
      {
        "acl-name": "blockbadhosts",
        "acl-description": "Block bad hosts",
        "acl-rules": {
          "acl-rule": [
            {
              "sequence": 1,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "203.0.113.14/32"
            },
            {
              "sequence": 2,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "203.0.113.15/32"
            },
            {
              "sequence": 555,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "5.5.5.5/32"
            },
            {
              "sequence": 5000,
              "acl-rule-description": "Default Permit",
              "action": "permit",
              "ip-version": "ipv4"
            }
          ]
        }
      }
    ]
  }

The cURL parameters and RESTCONF URL can be dissected as follows:

Item

Value

cURL Client Certificate

--cert ~/tnsr/tnsr-restconf-client.crt

cURL Client Certificate Key

--key ~/tnsr/tnsr-restconf-client.key

cURL CA Cert to validate TLS

--cacert ~/tnsr/tnsr-restconf-CA.crt

Request type (GET)

-X GET

RESTCONF Server protocol/host

https://tnsr.example.com

RESTCONF API location:

/restconf/data/

ACL config area (prefix:name)

netgate-acl:acl-config/

ACL table

acl-table/

ACL List, with restriction

acl-list=blockbadhosts

Note

Lists of items with a unique key can be restricted as shown above. The API documentation also calls this out as well, showing an optional ={name} in the query.

Retrieve a specific rule of a specific ACL

View only the default permit rule of the ACL:

Command:

$ curl -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  -X GET \
  https://tnsr.example.com/restconf/data/netgate-acl:acl-config/acl-table/acl-list=blockbadhosts/acl-rules/acl-rule=5000

Output:

{
    "netgate-acl:acl-rule": [
      {
        "sequence": 5000,
        "acl-rule-description": "Default Permit",
        "action": "permit",
        "ip-version": "ipv4"
      }
    ]
  }

The query is nearly identical to the previous one, with the following additional components:

Item

Value

ACL rules list

acl-rules/

ACL rule, with restriction

acl-rule=5000

Add a new rule to an existing ACL

Insert a new ACL rule entry with the following parameters:

Item

Value

Request Type

-X PUT (add content)

Content Type

-H "Content-Type: application/yang-data+json"

ACL Name

blockbadhosts

ACL Rule Sequence

10

ACL Rule Action

deny

ACL Rule Source Address

10.222.111.222/32

The new data passed in the -d parameter is JSON but with all whitespace removed so it can be more easily expressed on a command line.

Warning

The Content-Type header must be set when performing a write operation such as PUT or PATCH. The value of the header must reflect the type of data being sent. These examples use JSON, so the header is set to application/yang-data+json. When submitting XML, it would be application/yang-data+xml

The URL is the same as if the query is retrieving the rule in question.

Warning

Note the presence of the sequence number in both the supplied JSON data and in the URL. This must match.

Command:

$ curl -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  -H "Content-Type: application/yang-data+json" \
  -X PUT \
  -d '{"netgate-acl:acl-rule":[{"sequence": 10,"action":"deny","ip-version":"ipv4","src-ip-prefix":"10.222.111.222/32"}]}' \
  https://tnsr.example.com/restconf/data/netgate-acl:acl-config/acl-table/acl-list=blockbadhosts/acl-rules/acl-rule=10

Output: This command has no output when it works successfully.

Retrieve the contents of the ACL again to see that the new rule is now present:

Command:

$ curl -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  -X GET \
  https://tnsr.example.com/restconf/data/netgate-acl:acl-config/acl-table/acl-list=blockbadhosts

Output:

{
    "netgate-acl:acl-list": [
      {
        "acl-name": "blockbadhosts",
        "acl-description": "Block bad hosts",
        "acl-rules": {
          "acl-rule": [
            {
              "sequence": 1,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "203.0.113.14/32"
            },
            {
              "sequence": 2,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "203.0.113.15/32"
            },
            {
              "sequence": 10,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "10.222.111.222/32"
            },
            {
              "sequence": 555,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "5.5.5.5/32"
            },
            {
              "sequence": 5000,
              "acl-rule-description": "Default Permit",
              "action": "permit",
              "ip-version": "ipv4"
            }
          ]
        }
      }
    ]
  }

Use PATCH to update data

When using the PUT method, the client must supply all data in an entry to be replaced, even when only changing one small part. This makes it difficult to change, for example, the description of an ACL without sending the content of the ACL back in the request.

The PATCH method allows individual values to be replaced without requiring all of the data to be sent. With PATCH, the client need only send the modified values in a query, along with enough information to uniquely identify the entry.

For example, to update the description of the blockbadhosts ACL using PATCH, the client must only include the name of the ACL and the new description. It does not need to include the entire content of the ACL and its rules as it would with a PUT request.

Item

Value

Request Type

-X PATCH (change content)

Content Type

-H "Content-Type: application/yang-data+json"

ACL Name

blockbadhosts

ACL Description

Block packets from bad hosts

The command is formatted in a similar manner to the PUT request in the previous example.

Warning

The Content-Type header must be set when performing a write operation such as PUT or PATCH. The value of the header must reflect the type of data being sent. These examples use JSON, so the header is set to application/yang-data+json. When submitting XML, it would be application/yang-data+xml

Command:

$ curl -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  -H "Content-Type: application/yang-data+json" \
  -X PATCH \
  -d '{"netgate-acl:acl-list":[{"acl-name": "blockbadhosts","acl-description": "Block packets from bad hosts"}]}' \
  https://tnsr.example.com/restconf/data/netgate-acl:acl-config/acl-table/acl-list=blockbadhosts/

Output: This command has no output when it works successfully.

Retrieve the contents of the ACL again to see that the new description is now present:

Command:

$ curl -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  -X GET \
  https://tnsr.example.com/restconf/data/netgate-acl:acl-config/acl-table/acl-list=blockbadhosts

Output:

{
    "netgate-acl:acl-list": [
      {
        "acl-name": "blockbadhosts",
        "acl-description": "Block packets from bad hosts",
        "acl-rules": {
          "acl-rule": [
            {
              "sequence": 1,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "203.0.113.14/32"
            },
            {
              "sequence": 2,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "203.0.113.15/32"
            },
            {
              "sequence": 10,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "10.222.111.222/32"
            },
            {
              "sequence": 555,
              "action": "deny",
              "ip-version": "ipv4",
              "src-ip-prefix": "5.5.5.5/32"
            },
            {
              "sequence": 5000,
              "acl-rule-description": "Default Permit",
              "action": "permit",
              "ip-version": "ipv4"
            }
          ]
        }
      }
    ]
  }

Remove a specific rule from an ACL

Say that entry is no longer needed and it is safe to remove. That can be done with a DELETE request for the URL corresponding to its sequence number:

Command:

$ curl -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  -X DELETE \
  https://tnsr.example.com/restconf/data/netgate-acl:acl-config/acl-table/acl-list=blockbadhosts/acl-rules/acl-rule=10

Output: This does not produce any output if it completed successfully.

Retrieve the contents of the ACL again to confirm it was removed.

Use POST to retrieve data from endpoints which require parameters

There are API endpoints that return state, configuration, or similar data which require parameters to control the output. In these cases, rather than using GET, the request type is POST.

For example, there is an endpoint to retrieve BGP status but it also has several parameters to change or filter the output.

Item

Value

Request Type

-X POST (submit input parameters)

Content Type

-H "Content-Type: application/yang-data+json"

RESTCONF API location

/restconf/operations/netgate-bgp:bgp-show

Parameter Namespace

netgate-bgp:input

Parameter: request

neighbors

Parameter: family

ipv4

Parameter: vrf-id

default

Assemble these parameters into a query with cURL or similar tools:

$ curl -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  -H "Content-Type: application/yang-data+json" \
  -X POST \
  -d '{"netgate-bgp:input":[{"request":"neighbors","family":"ipv4","vrf-id":"default"}]}' \
  https://tnsr.example.com/restconf/operations/netgate-bgp:bgp-show

Output (trimmed for brevity):

{
  "netgate-bgp:output": {
    "stdout": "BGP neighbor is 10.2.222.2[...]"
  }
}

Handling shell output in RESTCONF responses

The previous example output from BGP status has output that is formatted for a terminal and not broken up into separate JSON fields. To improve handling of these types of responses, extract the data from the response and process it separately.

One way to accomplish this is with a JSON processing utility such as jq:

$ curl -s -f --cert ~/tnsr/tnsr-restconf-client.crt \
  --key ~/tnsr/tnsr-restconf-client.key \
  --cacert ~/tnsr/tnsr-restconf-CA.crt \
  -H "Content-Type: application/yang-data+json" \
  -X POST \
  -d '{"netgate-bgp:input":[{"request":"neighbors","family":"ipv4","vrf-id":"default"}]}' \
  https://tnsr.example.com/restconf/operations/netgate-bgp:bgp-show | \
  jq -r '."netgate-bgp:output" .stdout' | \
  grep 'BGP state'

After processing through jq and selecting the output node from the JSON response it is returned to typical terminal style output without the JSON encoding. At that point it can be run through typical shell utilities such as grep, as in the example above.

The output from this command is a single line indicating the state of one BGP peer:

BGP state = Established, up for 00:14:12

Adding More RESTCONF Users

To create additional RESTCONF users, only two actions are required on TNSR: Generate a certificate for the new user, and then add the user to NACM. This example adds a new user named anotheruser.

Generate a new user certificate:

tnsr(config)# pki private-key anotheruser generate key-length 4096
tnsr(config)# pki signing-request settings clear
tnsr(config)# pki signing-request set common-name anotheruser
tnsr(config)# pki signing-request set digest sha512
tnsr(config)# pki signing-request anotheruser generate
tnsr(config)# pki signing-request anotheruser sign ca-name restconf-CA days-valid 365 digest sha512 purpose client

Add this user to the NACM admin group:

tnsr(config)# nacm group admin
tnsr(config-nacm-group)# member anotheruser
tnsr(config-nacm-group)# exit

Then, the user certificate can be exported and copied to a new client and used as explained previously.