New to Kestra?
Use blueprints to kickstart your first workflows.
Watch an SFTP inbox, decrypt OpenPGP files, verify the partner's signature, refuse replayed files and quarantine anything that fails.
Banks, payment processors and payroll providers often deliver PGP-encrypted, signed files on an SFTP server. This blueprint accepts a file only if it decrypts with your key, passes its integrity check, carries a valid document signature from the partner key you configured, and its content was not accepted within replay_window. Every other file it picks up is moved to quarantine and the execution ends in WARNING.
watch_inbox (io.kestra.plugin.fs.sftp.Trigger) polls the inbox every minute for .pgp and .gpg files (any case, ASCII names), oldest first, downloads them and moves the originals to the received directory.each_file (io.kestra.plugin.core.flow.Loop) handles one file at a time inside an AllowFailure block:decrypt_and_verify (io.kestra.plugin.crypto.openpgp.Decrypt) checks decryption, integrity and a document signature by PARTNER_PGP_PUBLIC_KEY with the user ID allowed_signer.reject_replay fails if the same content was accepted within replay_window (KV store), whatever the file is called.archive_accepted moves the original to the accepted directory and remember_content stores the content hash. If any step fails, the errors branch moves it to quarantine instead.flag_not_accepted ends the execution in WARNING when at least one file was not accepted.concurrency.limit: 1 keeps two executions from checking the same content at once. Decrypted content never goes back to the SFTP server.
accepted: file, sha256 (of the decrypted content read as UTF-8 text, so it equals the file's SHA-256 only for UTF-8 or ASCII content), uri of the decrypted file in internal storage, encrypted_uri (the encrypted file as downloaded) and archived_as.not_accepted: file, iteration (whose logs give the exact message), status, reason, encrypted_uri and quarantined_as. The status is rejected (any failure of decrypt_and_verify, including a wrong private key or passphrase), duplicate (content already accepted) or error (verified, but a later step such as the SFTP move or the KV store failed; the content is not remembered, so the file can be re-sent).plugin-fs 2.11 or later (tested with 2.12.0).plugin-crypto 1.3.9 or later: earlier versions do not check the MDC or the final AEAD tag (before 1.3.7), decrypt only for the first recipient key (before 1.3.8) or accept signatures that are not document signatures (before 1.3.9).SFTP_HOST (port 22), SFTP_USERNAME, SFTP_PASSWORD, PGP_PRIVATE_KEY and PARTNER_PGP_PUBLIC_KEY (both ASCII-armored) and PGP_PRIVATE_KEY_PASSPHRASE.allowed_signer (the full user ID or its email, matched exactly; the example value rejects every real file) and replay_window (a duration in days or hours, default P90D; P3M, P1Y or P2W are rejected, and every valid file would end as error).variables and save the flow.gpg --encrypt --sign -r data-intake@example.com -u partner@example.com orders.csv, upload orders.csv.gpg under a temporary name that does not end in .pgp or .gpg (for example .orders.csv.gpg.part) and rename it.outputs.accepted lists the file; checksum mismatch in the decrypt_and_verify log means a wrong passphrase.The Execute button processes no files: there is no trigger context.
rejected. Test with a known-good file after any key change.Decrypt takes text mode from the unsigned one-pass header, so text whose line breaks are all CRLF also verifies with LF or CR (the replay key treats them as the same content). Decrypt does not check expiry, revocation, key flags, the hash algorithm, the signature time or the user ID certification, so trust rests on PARTNER_PGP_PUBLIC_KEY: replace it when the partner rotates a key. Only the outermost one-pass signature is verified.replay_window.error, text in other encodings (such as Windows-1252) can collide, and identical legitimate files are marked duplicate, so ask for a date or sequence number in the content.plugin-fs does not verify the host key, so a man in the middle could capture SFTP_PASSWORD or withhold files: use a trusted network, and prefer key-based authentication (keyfile) over a password. The trigger does not wait for uploads to finish (hence the rename) and moves files before the execution starts, replacing a same-name file left in the received directory: check that directory after an outage or while executions are queued, and ask for unique file names. Non-ASCII file names are skipped and stay in the inbox (tested with letters, digits, ., _ and -), because plugin-fs 2.12.0 garbles them when it moves them. Server copies can be changed by anyone with write access; encrypted_uri is the copy Kestra verified.io.kestra.plugin.core.execution.PurgeExecutions), and anyone who can read this flow's executions can download them. The replay lookup also reads KV entries of parent namespaces, so any flow that can write KV here or in a parent namespace can create or change the entries this flow relies on..asc names, hidden recipients (gpg --throw-keyids) and messages without a one-pass signature are not handled; up to 25 files per poll. Tested on Kestra 2.0.5 with GnuPG 2.4 messages (AEAD and v1 with MDC, compressed or not, binary and text-mode signatures) and Ed25519/Cv25519 and RSA 3072 keys.io.kestra.plugin.core.trigger.Flow trigger on this flow (states SUCCESS and WARNING) that reads each uri in trigger.outputs.accepted.allowed_signer and flow ID.