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:
Create a schedule configuration named
schedule-triggerSyncCheck.json
in your project'sconf
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 * * * * ? *`
.
The schedule calls a
scheduleTriggerSyncCheck.js
script, located in a directory namedproject-dir/script/sync
. Create thesync
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; }
Each generated scheduled task calls a script named
triggerSyncCheck.js
. Create that script in your project'sscript/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);
When you have set up the schedule configuration and the two scripts, you can test this key rotation as follows:
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
.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
).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
Shut down the server for keystore to be reloaded.
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", ... }
Add the corresponding
purpose
to thesecrets.json
file in themainKeyStore
code block:"idm.password.encryption2": { "types": [ "ENCRYPT", "DECRYPT" ], "aliases": [ { "alias": "my-new-key" } ] }
Restart the server and wait one minute for the first scheduled task to fire.
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.
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
Solely for the purpose of this example, in
managed.json
, set"scope" : "public"
to expose the applied password encryption key.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"
Add the newly created
my-new-key
alias to yourconf/secrets.json
file, in theidm.password.encryption
code block:"idm.password.encryption": { "types": [ "ENCRYPT", "DECRYPT" ], "aliases": [ "my-new-key", "&{openidm.config.crypto.alias|openidm-sym-default}" ] }
To apply the new key to your configuration, shut down and restart IDM.
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"
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"
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==" } } }, ...