Ratnakar7
Mega Sage

When organizations need to export sensitive HRSD or PII data to external data lakes or SFTP servers, encryption is not just a best practice - it's often a compliance requirement. ServiceNow doesn't provide native PGP encryption capabilities, so this guide details a production-ready solution using the OpenPGP.js library to encrypt files directly within the platform.

 

Payroll HR cases are exactly where attachments tend to be the most sensitive. The ask I kept hearing was simple:

"When a user attaches a file to a payroll case, create an encrypted version automatically - no manual step."

->Business Value:

- Secure transmission of sensitive HR data to external systems

- Compliance with data protection regulations (GDPR, HIPAA, SOX)

- End-to-end encryption without relying on external encryption tools

- Automated, scheduled encryption workflows

->Solution Components:

1. UI Script integration with OpenPGP.js library

2. UI Page for key generation and management

3. Script Include for encryption/decryption operations

4. Scheduled job for automated HRSD data exports

5. Business rules for on-demand file encryption

 

-> Architecture Overview

Ratnakar7_0-1770889782580.png

This post shares what worked for me in a PDI and how I structured it so it can evolve into a production-grade automation pattern.

You'll get two parts:

  1. Classic UI POC: Encrypt an attachment with a customer/recipient public key, and write back <file>.pgp on the same record. This uses OpenPGP.js in the browser (WebCrypto-backed).
  2. Backend automation (queue + retry): A "no UI required" pattern where ServiceNow picks up new attachments from sys_attachment, calls a small self-hosted OpenPGP encryption API (GnuPG-based), and writes the encrypted file back to the original payroll case within a minute. ServiceNow’s Attachment API behaviors and constraints (size/extensions/content types) are respected throughout.

Ratnakar7_1-1770890340042.png

Queue + retry pattern
Ratnakar7_2-1770890733609.png

Why OpenPGP/PGP for payroll attachments?

OpenPGP is a solid fit because you encrypt with the recipient's public key and only the recipient can decrypt with their private key. That's the typical, portable "send encrypted files to someone else" model that works across toolchains.
OpenPGP encryption is also hybrid (session key + public key wrapping), so it scales far better than "pure asymmetric encryption" for larger files.


Part 1 - Classic UI Page POC 

This section is what I already validated with OpenPGP.js Library Integration.

Step 1: Create UI Script for OpenPGP Library

Navigate to: System UI > UI Scripts

Name: 'OpenPGP_Library'

Global: 'true'

Active: 'true'

UI Type: 'All'

The OpenPGP.js library provides RSA and ECC encryption capabilities. We'll use version 5.x which supports modern encryption standards.

Note: Due to the size of the OpenPGP.js library (~500KB minified), you should:

1. Download the library from: https://unpkg.com/openpgp@5.11.0/dist/openpgp.min.js

2. Copy the minified content into the UI Script

3. Or reference it via CDN in your UI Page (less recommended for production)

 

Step 2: Create PGP Key Management UI Page

Navigate to: System UI > UI Pages

Name: 'pgp_key_manager'

Description: 'PGP Key Generation and Management Interface'

This UI Page provides administrators with:

- RSA key pair generation (2048-bit or 4096-bit)

- Public/Private key storage in secure system properties

- Key import/export capabilities

- Key validation and testing

The interface allows security administrators to:

1. Generate new PGP key pairs

2. Import existing keys from external systems

3. Export public keys for sharing with data recipients

4. Test encryption/decryption operations

 

Step 3: Testing :

- Navigate to HR case record and attach a file,
- Navigate to sys_attachment table, search for the recently attached file,
- Right-click on the record and copy the sys_id

Ratnakar7_0-1770913032580.png

 

-Navigate to the UI page (created in step 2):
- Paste an attachment sys_id,

- Paste the customer public key (for POC, you can generate it from any online pgp key generator [https://pgpkeygen.com/])

- Click Encrypt+Upload,

Ratnakar7_1-1770913511899.png


-and you get <file>.pgp attached back on the same payroll case. 

Ratnakar7_4-1770913758144.png

Instead of dumping full scripts into the post, I referenced these functions in my code and kept the blog readable:

normalizeSysId(raw) // extracts a 32-hex sys_id from a pasted value (sys_id, URL, etc.).
fetchAttachmentMetadata(sys_id) //  calls GET /api/now/attachment/{sys_id}.
fetchAttachmentBytes(sys_id) // calls GET /api/now/attachment/{sys_id}/file.
uploadAttachmentToRecord(table, recordId, name, bytes, contentType) //  uses POST /api/now/attachment/upload (multipart) and fallback to POST /api/now/attachment/file?...; the Attachment API stores content type and respects configured limits.
encryptAndUpload() //  encrypts the attachment bytes with the recipient public key (OpenPGP hybrid model).
decryptAndAttach()// (POC-only) - decrypts using private key input; in real deployments customers typically decrypt with GnuPG outside ServiceNow.

Key generation outside ServiceNow (what customers do)

Customers typically generate keys and decrypt using GnuPG. The GnuPG handbook documents keypairs and encrypt/decrypt flows.
A common workflow is:

  • Generate keys: gpg --full-generate-key
  • Export public key: gpg --export --armor <id/email> > public.asc
  • Decrypt file: gpg -d -o output.file input.file.pgp

Part 2 - Production automation: "attach → wait 1 minute → see <file>.pgp on the same payroll case"

This section is the practical answer to "how do I automate it".

Design goals

  • Encrypt all attachments added to sn_hr_core_case_payroll.
  • Don't slow down the HR user's upload transaction.
  • Keep robust retry behavior (network blips, encryption API unavailable, etc.).
  • Write the encrypted result back to the same record using ServiceNow attachment handling. The platform's Attachment API and attachment subsystem respect max size/allowed types and store content-type metadata.

Step 1 - Minimal configuration (properties)

Create these system properties (for POC/automation):

  • u.pgp.encrypt_api_url → https://<your-encrypt-api-host>/encrypt
  • u.pgp.partner_public_key_armored → paste the recipient/customer public key (ASCII armored)
  • u.pgp.batch_size → 5
  • u.pgp.max_attempts → 6
  • u.pgp.base_delay_sec → 60

For production, replace "one global public key" with a Partner Key table keyed by customer/COE/vendor. (Not needed for the POC automation.)


Step 2 - Create the queue table: u_pgp_export_queue

Fields (minimum set):

  • u_status (NEW, IN_PROGRESS, SENT, FAILED, DEAD)
  • u_attachment_sys_id
  • u_source_table
  • u_source_sys_id
  • u_file_name
  • u_content_type
  • u_attempt_count
  • u_next_attempt_at
  • u_last_error
  • u_correlation_id

Index recommendation:

  • (u_status, u_next_attempt_at) for fast worker polling.

Step 3 - Business Rule: enqueue on sys_attachment insert (Payroll only, all attachments)

Table: sys_attachment
When: After Insert
Purpose: Create a queue row; no encryption in the upload transaction.

(function executeRule(current, previous) {

  // Only encrypt attachments added to Payroll HR cases
  if (current.table_name.toString() !== 'sn_hr_core_case_payroll')
    return;

  // Avoid recursion: if we already created .pgp, don't re-queue it
  var fn = (current.file_name + '').toString();
  if (fn.endsWith('.pgp') || fn.endsWith('.gpg'))
    return;

  var q = new GlideRecord('u_pgp_export_queue');
  q.initialize();
  q.u_status = 'NEW';
  q.u_attachment_sys_id = current.sys_id.toString();
  q.u_source_table = current.table_name.toString();     // sn_hr_core_case_payroll
  q.u_source_sys_id = current.table_sys_id.toString();  // payroll case sys_id
  q.u_file_name = fn;
  q.u_content_type = (current.content_type + '').toString();
  q.u_attempt_count = 0;
  q.u_next_attempt_at = gs.nowDateTime();
  q.u_correlation_id = gs.generateGUID();
  q.insert();

})(current, previous);

Step 4 - External encryption endpoint (free/open-source, but self-hosted)

For a POC, don't use a random "free encryption API" on the internet for HR data. Instead, run a tiny open-source service in your network.

Why this service model

  • GnuPG is a standard OpenPGP implementation used for encryption/decryption/key management.
  • python-gnupg is a wrapper that uses the GnuPG executable, which makes it easy to build a small REST wrapper for encryption.

(If you want a container image with GPG tooling for key generation/export, Docker Hub also provides a GnuPG image with example commands. )


Step 5 - Script Include: PGPExportProcessor (queue + retry + encrypt + attach)

This worker:

  • reads due queue items
  • reads attachment bytes
  • calls encrypt API
  • writes <file>.pgp back to the same payroll case
  • retries with exponential backoff
var PGPExportProcessor = Class.create();
PGPExportProcessor.prototype = {
  initialize: function() {
    this.BATCH_SIZE = parseInt(gs.getProperty('u.pgp.batch_size', '5'), 10);
    this.MAX_ATTEMPTS = parseInt(gs.getProperty('u.pgp.max_attempts', '6'), 10);
    this.BASE_DELAY_SEC = parseInt(gs.getProperty('u.pgp.base_delay_sec', '60'), 10);

    this.ENCRYPT_API_URL = gs.getProperty('u.pgp.encrypt_api_url', '');
    this.PARTNER_PUBLIC_KEY = gs.getProperty('u.pgp.partner_public_key_armored', '');
  },

  runBatch: function() {
    var gr = new GlideRecord('u_pgp_export_queue');
    gr.addQuery('u_status', 'IN', 'NEW,FAILED');
    gr.addQuery('u_next_attempt_at', '<=', gs.nowDateTime());
    gr.orderBy('u_next_attempt_at');
    gr.setLimit(this.BATCH_SIZE);
    gr.query();

    while (gr.next()) {
      this._processOne(gr);
    }
  },

  _processOne: function(job) {
    try {
      job.u_status = 'IN_PROGRESS';
      job.update();

      if (!this.ENCRYPT_API_URL) throw 'Missing property u.pgp.encrypt_api_url';
      if (!this.PARTNER_PUBLIC_KEY) throw 'Missing property u.pgp.partner_public_key_armored';

      // 1) Read attachment bytes
      var attGr = new GlideRecord('sys_attachment');
      if (!attGr.get(job.u_attachment_sys_id)) throw 'Attachment not found: ' + job.u_attachment_sys_id;

      var gsa = new GlideSysAttachment();
      var bytes = gsa.getBytes(attGr);

      // 2) Call encrypt API (base64 payload)
      var payloadB64 = GlideStringUtil.base64Encode(bytes);
      var reqBody = {
        public_key_armored: this.PARTNER_PUBLIC_KEY,
        file_name: job.u_file_name.toString(),
        payload_b64: payloadB64
      };

      var rm = new sn_ws.RESTMessageV2();
      rm.setHttpMethod('POST');
      rm.setEndpoint(this.ENCRYPT_API_URL);
      rm.setRequestHeader('Accept', 'application/json');
      rm.setRequestHeader('Content-Type', 'application/json');
      rm.setRequestBody(JSON.stringify(reqBody));

      // Runs from scheduled job context (safe for POC)
      var resp = rm.execute();
      var code = resp.getStatusCode();
      var body = resp.getBody();

      if (code !== 200) throw 'Encrypt API failed HTTP ' + code + ': ' + body;

      var obj = JSON.parse(body);
      if (!obj.encrypted_b64) throw 'Encrypt API response missing encrypted_b64';

      var encryptedBytes = GlideStringUtil.base64DecodeAsBytes(obj.encrypted_b64);

      // 3) Write encrypted attachment back to SAME payroll case record.
      // Attachment subsystem enforces instance attachment rules and stores content-type metadata. 
      gsa.write(job.u_source_table, job.u_source_sys_id, job.u_file_name + '.pgp', 'application/octet-stream', encryptedBytes);

      job.u_status = 'SENT';
      job.u_last_error = '';
      job.update();

    } catch (ex) {
      this._handleFailure(job, ex);
    }
  },

  _handleFailure: function(job, ex) {
    var attempts = (parseInt(job.u_attempt_count + '', 10) || 0) + 1;
    job.u_attempt_count = attempts;
    job.u_last_error = (ex && ex.message) ? ex.message : ('' + ex);

    if (attempts >= this.MAX_ATTEMPTS) {
      job.u_status = 'DEAD';
      job.u_next_attempt_at = '';
    } else {
      job.u_status = 'FAILED';
      var delay = Math.min(this.BASE_DELAY_SEC * Math.pow(2, attempts - 1), 3600);
      job.u_next_attempt_at = gs.dateAdd(gs.nowDateTime(), delay, 'second');
    }
    job.update();
  },

  type: 'PGPExportProcessor'
};
 

Step 6 - Scheduled Job: every minute

Create a Scheduled Script Execution to run every 1 minute:

 
(function() {
  new PGPExportProcessor().runBatch();
})();

-> Delivery options (after encryption)

Once you have .pgp attachments on the case, delivery becomes a separate concern:

If customer gives AWS S3

- Use pre-signed URLs so ServiceNow can upload encrypted objects without storing AWS credentials; AWS documents presigned uploads as a standard approach.

If customer wants SFTP

- A common AWS-native alternative is AWS Transfer Family which supports SFTP/FTPS/FTP directly into S3.

 


-> Security Best Practices

Key Management

1. Private Key Storage: Never store private keys in plain text. Use ServiceNow's encrypted system properties.

2. Passphrase Protection: Use strong passphrases (min. 16 characters) for private key encryption.

3. Key Rotation: Rotate PGP keys annually or when personnel changes occur.

4. Access Control: Restrict key management UI Page to security admins only (ACL: 'security_admin' role).


Data Protection

1. Encryption Strength: Use minimum 2048-bit RSA or ECC 256-bit keys.

2. Secure Deletion: Overwrite unencrypted temporary files after encryption.

3. Audit Logging: Log all encryption/decryption operations with user tracking.

4. Transport Security: Use SFTP/FTPS for encrypted file transfers (never plain FTP).


Compliance Considerations

- GDPR: Encryption satisfies "appropriate technical measures" requirement

- HIPAA: PGP encryption qualifies as addressable implementation specification

- SOX: Provides data integrity and confidentiality controls

- PCI-DSS: Supports cardholder data protection requirements

 

Performance Optimization

Large File Handling

For files > 10MB:

1. Implement chunked encryption (process in 1MB segments)

2. Use asynchronous processing with progress tracking

3. Consider compression before encryption (reduces file size by ~60%)


 -> Advanced Use Cases

Multi-Recipient Encryption

Encrypt files for multiple recipients (e.g., HR, Finance, Legal):

var publicKeys = [hrPublicKey, financePublicKey, legalPublicKey];
var encrypted = pgpUtil.encryptForMultipleRecipients(data, publicKeys);

 

Digital Signatures

Add digital signatures to verify data authenticity:

var signed = pgpUtil.signAndEncrypt(data, senderPrivateKey, recipientPublicKey);

 

Encryption with Compression

Reduce file size before encryption:

var compressed = new GlideTextReader().compress(data);
var encrypted = pgpUtil.encryptData(compressed, publicKey);

 

Conclusion

This PGP encryption solution bridges a critical gap in ServiceNow's native capabilities, enabling secure transmission of sensitive HRSD data to external systems. By leveraging the OpenPGP.js library and following the POC and automation pattern above, organizations can achieve:

 Compliance with data protection regulations

 Security through industry-standard encryption

 Automation via scheduled jobs and business rules

 Flexibility with multiple encryption workflows

 


Thanks,
Ratnakar