Changing and Rotating Encryption Keys

Most regulatory requirements mandate that the keys used to decrypt sensitive data be rotated out and replaced with new keys on a regular basis. The main purpose of rotating encryption keys is to reduce the amount of data encrypted with that key, so that the potential impact of a security breach with a specific key is reduced. You can update encryption keys in several ways, including the following:

Rotating Encryption Keys Manually

IDM evaluates keys in secrets.json sequentially. For example, assume that you have added a new key named my-new-key to the keystore, as described in "Using CA-Signed Certificates".

To use this new key to encrypt passwords, you would include my-new-key as the first alias in the idm.password.encryption secret, as follows:

{
    "secretId" : "idm.password.encryption",
    "types": [ "ENCRYPT", "DECRYPT" ],
    "aliases": [ "my-new-key", "&{openidm.config.crypto.alias|openidm-sym-default}" ]
}

The properties that use this key (in this case, passwords) are re-encrypted with the new key the next time the managed object is updated. You do not need to restart the server.

Important

If you rotate an encryption key, the active encryption key might not be the correct key to use for decryption of properties that have already been encrypted with a previous key.

You must therefore keep all applicable keys in secrets.json until every object that is encrypted with old keys have been updated with the latest key.

You can force key rotation on all managed objects by running the triggerSyncCheck action on the entire managed object data set. The triggerSyncCheck action examines the crypto blob of each object and updates the encrypted property with the correct key.

For example, the following command forces all managed user objects to use the new key:

curl \
--header "X-OpenIDM-Username: openidm-admin" \
--header "X-OpenIDM-Password: openidm-admin" \
--header "Accept-API-Version: resource=1.0" \
--cacert ca-cert.pem \
--header "Content-Type: application/json" \
--request POST \
"https://localhost:8443/openidm/managed/user/?_action=triggerSyncCheck"
{
    "status": "OK",
    "countTriggered": 10
}

In a large managed object set, the triggerSyncCheck action can take a long time to run on only a single node. You should therefore avoid using this action if your data set is large. An alternative to running triggerSyncCheck over the entire data set is to iterate over the managed data set and call triggerSyncCheck on each individual managed object. You can call this action manually or by using a script.

The following example shows the manual commands that must be run to launch the triggerSyncCheck action on all managed users. The first command uses a query filter to return all managed user IDs. The second command iterates over the returned IDs calling triggerSyncCheck on each ID:

curl \
--header "X-OpenIDM-Username: openidm-admin" \
--header "X-OpenIDM-Password: openidm-admin" \
--header "Accept-API-Version: resource=1.0" \
--cacert ca-cert.pem \
"https://localhost:8443/openidm/managed/user?_queryFilter=true&_fields=_id"
{
  "result": [
    {
      "_id": "9dce06d4-2fc1-4830-a92b-bd35c2f6bcbb",
      "_rev": "000000004988917b"
    },
    {
      "_id": "55ef0a75-f261-47e9-a72b-f5c61c32d339",
      "_rev": "00000000dd89d671"
    },
    {
      "_id": "998a6181-d694-466a-a373-759a05840555",
      "_rev": "000000006fea54ad"
    },
    ...
  ]
}
curl \
--header "X-OpenIDM-Username: openidm-admin" \
--header "X-OpenIDM-Password: openidm-admin" \
--header "Accept-API-Version: resource=1.0" \
--cacert ca-cert.pem \
--header "Content-Type: application/json" \
--request POST \
"https://localhost:8443/openidm/managed/user/9dce06d4-2fc1-4830-a92b-bd35c2f6bcbb?_action=triggerSyncCheck"

In large data sets, the most efficient way to achieve key rotation is to use the scheduler service to launch these commands. The following section shows how to use the scheduler service for this purpose.

Using Scheduled Tasks to Rotate Keys

This example uses a script to generate multiple scheduled tasks. Each scheduled task iterates over a subset of the managed object set (defined by the pageSize). The generated scheduled task then calls another script that launches the triggerSyncCheck action on each managed object in that subset.

You can set up a similar schedule as follows:

  1. Create a schedule configuration named schedule-triggerSyncCheck.json in your project's conf directory. That schedule should look as follows:

    {
        "enabled" : true,
        "persisted" : true,
        "type" : "cron",
        "schedule" : "0 * * * * ? *",
        "concurrentExecution" : false,
        "invokeService" : "script",
        "invokeContext" : {
            "waitForCompletion" : false,
            "script": {
                "type": "text/javascript",
                "name": "sync/scheduleTriggerSyncCheck.js"
            },
            "input": {
                "pageSize": 2,
                "managedObjectPath" : "managed/user",
                "quartzSchedule" : "0 * * * * ? *"
            }
        }
    }

    You can change the following parameters of this schedule configuration to suit your deployment:

    pageSize

    The number of objects that each generated schedule will handle. This value should be high enough not to create too many schedules. The number of schedules that is generated is equal to the number of objects in the managed object store, divided by the page size.

    For example, if there are 500 managed users and a page size of 100, five schedules will be generated (500/100).

    managedObjectPath

    The managed object set over which the scheduler iterates. For example, managed/user if you want to iterate over the managed user object set.

    quartzSchedule

    The schedule at which these tasks should run. For example, to run the task every minute, this value would be `0 * * * * ? *`.

  2. The schedule calls a scheduleTriggerSyncCheck.js script, located in a directory named project-dir/script/sync. Create the sync directory, and add that script as follows:

    var managedObjectPath = object.managedObjectPath;
    var pageSize = object.pageSize;
    var quartzSchedule = object.quartzSchedule;
    
    var managedObjects = openidm.query(managedObjectPath, {
        "_queryFilter": "true",
        "_fields": "_id"
    });
    
    var numberOfManagedObjects = managedObjects.result.length;
    
    for (var i = 0; i < numberOfManagedObjects; i += pageSize) {
        var scheduleId = java.util.UUID.randomUUID().toString();
        var ids = managedObjects.result.slice(i, i + pageSize).map(function(obj) {
            return obj._id
        });
        var schedule = newSchedule(scheduleId, ids);
        openidm.create("/scheduler", scheduleId, schedule);
    }
    
    function newSchedule(scheduleId, ids) {
        var schedule = {
            "enabled": true,
            "persisted": true,
            "type": "cron",
            "schedule": quartzSchedule,
            "concurrentExecution": false,
            "invokeService": "script",
            "invokeContext": {
                "waitForCompletion": true,
                "script": {
                    "type": "text/javascript",
                    "name": "sync/triggerSyncCheck.js"
                },
                "input": {
                    "ids": ids,
                    "managedObjectPath": managedObjectPath,
                    "scheduleId": scheduleId
                }
            }
        };
        return schedule;
    }
  3. Each generated scheduled task calls a script named triggerSyncCheck.js. Create that script in your project's script/sync directory. The contents of the script are as follows:

    var ids = object.ids;
    var scheduleId = object.scheduleId;
    var managedObjectPath = object.managedObjectPath;
    
    for (var i = 0; i & lt; ids.length; i++) {
        openidm.action(managedObjectPath + "/" + ids[i], "triggerSyncCheck", {}, {});
    }
    
    openidm.delete("scheduler/" + scheduleId, null);
  4. When you have set up the schedule configuration and the two scripts, you can test this key rotation as follows:

    1. Edit your project's conf/managed.json file to return user passwords by default by setting "scope" : "public".

      "password" : {
          ...
          "encryption" : {
              "purpose" : "idm.password.encryption"
          },
          "scope" : "public",
          ...
      }

      Because passwords are not returned by default, you will not be able to see the new encryption on the password unless you change the property's scope.

    2. Perform a GET request to return any managed user entry in your data set. For example:

      curl \
      --header "X-OpenIDM-Username: openidm-admin" \
      --header "X-OpenIDM-Password: openidm-admin" \
      --header "Accept-API-Version: resource=1.0" \
      --cacert ca-cert.pem \
      --request GET \
      "https://localhost:8443/openidm/managed/user/ccd92204-aee6-4159-879a-46eeb4362807"
      {
        "_id" : "ccd92204-aee6-4159-879a-46eeb4362807",
        "_rev" : "0000000009441230",
        "preferences" : {
          "updates" : false,
          "marketing" : false
        },
        "mail" : "bjensen@example.com",
        "sn" : "Jensen",
        "givenName" : "Babs",
        "userName" : "bjensen",
        "password" : {
          "$crypto" : {
            "type" : "x-simple-encryption",
            "value" : {
              "cipher" : "AES/CBC/PKCS5Padding",
              "stableId" : "openidm-sym-default",
              "salt" : "CVrKDuzfzunXfTDbCwU1Rw==",
              "data" : "1I5tWT5aRH/12hf5DgofXA==",
              "keySize" : 16,
              "purpose" : "idm.password.encryption",
              "iv" : "LGE+jnC3ZtyvrE5pfuSvtA==",
              "mac" : "BEXQ1mftxA63dXhJO6dDZQ=="
            }
          }
        },
        "accountStatus" : "active",
        "effectiveRoles" : [ ],
        "effectiveAssignments" : [ ]
      }

      Notice that the user's password is encrypted with the default encryption key (openidm-sym-default).

    3. Create a new encryption key in the IDM keystore:

      keytool \
      -genseckey \
      -alias my-new-key \
      -keyalg AES \
      -keysize 128 \
      -keystore /path/to/openidm/security/keystore.jceks \
      -storetype JCEKS
    4. Shut down the server for keystore to be reloaded.

    5. Change your project's conf/managed.json file to change the encryption purpose for managed user passwords:

      "password" : {
          ...
          "encryption" : {
              "purpose" : "idm.password.encryption2"
          },
          "scope" : "public",
          ...
      }
    6. Add the corresponding purpose to the secrets.json file in the mainKeyStore code block:

      "idm.password.encryption2": {
        "types": [ "ENCRYPT", "DECRYPT" ],
        "aliases": [
          {
            "alias": "my-new-key"
          }
        ]
      }
    7. Restart the server and wait one minute for the first scheduled task to fire.

    8. Perform a GET request again to return the entry of the managed user that you returned previously:

      curl \
      --header "X-OpenIDM-Username: openidm-admin" \
      --header "X-OpenIDM-Password: openidm-admin" \
      --header "Accept-API-Version: resource=1.0" \
      --cacert ca-cert.pem \
      --request GET \
      "https://localhost:8443/openidm/managed/user/ccd92204-aee6-4159-879a-46eeb4362807"
      {
        "_id" : "ccd92204-aee6-4159-879a-46eeb4362807",
        "_rev" : "0000000009441230",
        "preferences" : {
          "updates" : false,
          "marketing" : false
        },
        "mail" : "bjensen@example.com",
        "sn" : "Jensen",
        "givenName" : "Babs",
        "userName" : "bjensen",
        "password" : {
          "$crypto" : {
            "type" : "x-simple-encryption",
            "value" : {
              "cipher" : "AES/CBC/PKCS5Padding",
              "stableId" : "my-new-key",
              "salt" : "CVrKDuzfzunXfTDbCwU1Rw==",
              "data" : "1I5tWT5aRH/12hf5DgofXA==",
              "keySize" : 16,
              "purpose" : "idm.password.encryption2",
              "iv" : "LGE+jnC3ZtyvrE5pfuSvtA==",
              "mac" : "BEXQ1mftxA63dXhJO6dDZQ=="
            }
          }
        },
        "accountStatus" : "active",
        "effectiveRoles" : [ ],
        "effectiveAssignments" : [ ]
      }

      Notice that the user password is now encrypted with my-new-key.

Changing the Active Alias for Managed Object Encryption

This example describes how you can configure and then change the managed object encryption key with a scheduled task. You'll create a new key, set up a managed user, add the key to secrets.json, restart IDM, run a triggerSyncCheck, and review the result.

  1. Create a new key for the IDM keystore in the security/keystore.jceks file:

    keytool \
    -genseckey \
    -alias my-new-key \
    -keyalg AES \
    -keysize 128 \
    -keystore /path/to/openidm/security/keystore.jceks \
    -storetype JCEKS
  2. Solely for the purpose of this example, in managed.json, set "scope" : "public" to expose the applied password encryption key.

  3. Create a managed user:

    curl \
    --header "X-OpenIDM-Username: openidm-admin" \
    --header "X-OpenIDM-Password: openidm-admin" \
    --header "Accept-API-Version: resource=1.0" \
    --cacert ca-cert.pem \
    --header "Content-Type: application/json" \
    --request PUT \
    --data '{
      "userName": "rsutter",
      "sn": "Sutter",
      "givenName": "Rick",
      "mail": "rick@example.com",
      "telephoneNumber": "6669876987",
      "description": "Another user",
      "country": "USA",
      "password": "Passw0rd"
    }' \
    "https://localhost:8443/openidm/managed/user/ricksutter"
  4. Add the newly created my-new-key alias to your conf/secrets.json file, in the idm.password.encryption code block:

    "idm.password.encryption": {
      "types": [ "ENCRYPT", "DECRYPT" ],
      "aliases": [ "my-new-key", "&{openidm.config.crypto.alias|openidm-sym-default}" ]
    }
  5. To apply the new key to your configuration, shut down and restart IDM.

  6. Force IDM to update the key for your users with the triggerSyncCheck action:

    curl \
    --header "X-OpenIDM-Username: openidm-admin" \
    --header "X-OpenIDM-Password: openidm-admin" \
    --header "Accept-API-Version: resource=1.0" \
    --cacert ca-cert.pem \
    --header "Content-Type: application/json" \
    --request POST \
    "https://localhost:8443/openidm/managed/user/?_action=triggerSyncCheck"
  7. Review the result for the newly created user, ricksutter:

    curl \
    --header "X-OpenIDM-Username: openidm-admin" \
    --header "X-OpenIDM-Password: openidm-admin" \
    --header "Accept-API-Version: resource=1.0" \
    --cacert ca-cert.pem \
    --request GET  \
    "https://localhost:8443/openidm/managed/user/ricksutter"
  8. In the output, you should see the new my-new-key encryption key applied to that user's password:

    ...
     "password": {
       "$crypto": {
         "type": "x-simple-encryption",
         "value": {
           "cipher": "AES/CBC/PKCS5Padding",
           "stableId": "my-new-key",
           "salt": "bGyKG3PKmwHONOfxerr1Qg==",
           "data": "6vXZiJ3ZNN/UUnsrT7dTQw==",
           "keySize": 16,
           "purpose": "idm.password.encryption",
           "iv": "doAdtxfWfFbrPIIfubGi5g==",
           "mac": "OML6xd9qvDtD5AvMc1Tc3A=="
         }
       }
     },
    ...  
Read a different version of :