Where Scheduled Jobs Should Keep Their Credentials
An unattended job has a problem no interactive session has: it must authenticate — to a partner's SFTP server, to a file share, to the scheduler itself — with nobody there to type anything. Whatever proves the job's identity therefore has to be stored, at rest, on a disk, before the job runs. There is no clever trick that avoids this. Every scheduled transfer job in the world keeps a secret somewhere; the only real questions are where, who else can read it there, and what happens on the day it has to change.
This article answers those questions for both platforms. It walks the places secrets leak from first — scripts, command lines, repositories — then the honest options: SSH keys and their file permissions, Windows Credential Manager and DPAPI-protected files, Task Scheduler's own stored password, and the plain-config-file last resort on the cron side. It ends with the part most guides skip: rotating a job credential without breaking the schedule that depends on it. It is part of our Scheduled Jobs series and assumes the job already runs as a dedicated identity, per service accounts for scheduled jobs.
Two Questions That Sort Every Option
Evaluating a credential home takes exactly two questions. First: who can read the secret there? Not "is it encrypted" — encrypted-at-rest secrets must still be decryptable by the job, so the real boundary is which accounts and roles can get at the usable form. The honest audiences to count: the service account itself (necessary), local administrators (almost always, whatever the mechanism), whoever can read backups of that disk, and — for badly placed secrets — everyone with repo access or a process list. Second: what does rotation cost? A secret you cannot change without an outage will not be changed, and an unrotatable credential grows more dangerous every month it survives. Good homes make the audience small and the rotation cheap. Everything below is ranked by those two measures.
Where Job Secrets Leak From
Before the good homes, the common ones. Each of these is something you will find in an inherited estate, and each has a specific audience problem:
- Hardcoded in the script. The password now travels with the file: into version control and its eternal history, onto the file share, into every backup, into the email where someone sent "the latest version" for review. The audience is unknowable, which is the worst property a secret can have.
- On the command line. Arguments are not private. Any user on a Unix box can see another process's argv in
psoutput; on Windows the command line is a column in Task Manager and appears in task definitions thatschtasks /queryhappily prints. Cron makes it worse by logging the command it ran to the system log. A password passed as a flag is published, gently, in several places at once. - In environment variables. Better than argv, still weak: other processes of the same user, debugging dumps, and the reflex of echoing the environment into logs all expose it. Treat environment injection as a transport from a real secret store, not as the store.
- In a world-readable config file. The classic quiet failure — the script is careful, the
config.ininext to it is readable by every account on the machine. - In the job's own log. Scripts that trace their commands (
set -xin bash, verbose flags in transfer clients) copy the secret into a file with entirely different, usually looser, permissions.
Here is the process-list leak in the wild, because seeing it once is worth a page of warnings:
$ ps -ef | grep lftp
svc-tran 4127 4101 lftp -u partner,Winter#Files9 sftp://sftp.partnerco.example
^ every local user can read this line while the transfer runs
Remember: a secret's real storage location is every place it is readable, not the place you meant to put it. Scripts, command lines, environments, and logs are all storage — just storage you do not control.
First Choice: A Key Instead of a Password
Where the protocol supports it — SFTP, above all — the best stored credential is an SSH private key, because it is designed to be a file. There is no password to weave into commands, the public half on the server is harmless to expose, and rotation is a deliberate, servable operation rather than a domain policy event. Generation and handling are covered in generating and storing SSH keys; what matters here is the storage discipline for an unattended job:
# Linux: the key lives in the service user's home, readable by that user only
chmod 700 /home/svc-transfer/.ssh
chmod 600 /home/svc-transfer/.ssh/invoice_push_key
:: Windows: same idea with ACLs -- strip inheritance, grant only what must read
icacls C:\Jobs\InvoicePush\secret /inheritance:r ^
/grant:r "svc-invoicepush:(OI)(CI)R" "SYSTEM:(OI)(CI)F" "Administrators:(OI)(CI)F"
Now the honest part, because it is the most argued question in unattended transfer work: the passphrase. A private key can be encrypted with a passphrase, and for a human's key it always should be. But a scheduled job has no human to type it. An agent process that holds the unlocked key can bridge the gap in interactive setups; wiring one reliably into cron or Task Scheduler adds machinery that itself must survive reboots. So the standard, defensible position for job keys is: no passphrase, compensated hard — file permissions exactly as above, a dedicated key per flow (never a person's key, never one key shared across systems), a remote account scoped so the key unlocks as little as possible, and rotation plus monitoring treated as first-class. Write that trade into the job's documentation so a reviewer sees a decision, not an oversight.
Windows Homes for Job Secrets, Ranked
When the credential must be a password — an FTPS login, a share credential — Windows offers three honest homes and one unavoidable one.
The unavoidable one first: Task Scheduler's stored logon. A task set to run whether the user is logged on or not stores the service account's Windows password so the scheduler can log it on; that is the mechanism, as covered in the Task Scheduler walkthrough. It is protected by the operating system's secret store, and the honest caveat is that local administrators can extract such secrets — so it counts admins into the audience, and it makes "who is an admin on the job machine" part of your credential policy. Accept it, keep the account least-privilege, and move on: this stored logon is for Windows itself, not a place to keep transfer passwords.
For transfer passwords, DPAPI-protected files. DPAPI (the built-in data protection interface) encrypts data so that, in its default per-user mode, only the same account on the same machine can decrypt it — which is precisely the shape a job secret wants. PowerShell exposes it directly:
:: ONCE, in a session running AS the service account: :: runas /user:CORP\svc-invoicepush powershell PS> Get-Credential partnerftp | Export-Clixml C:\Jobs\InvoicePush\secret\partner.cred # in the scheduled script, every run: $cred = Import-Clixml C:\Jobs\InvoicePush\secret\partner.cred # $cred.UserName, and $cred.GetNetworkCredential().Password when a tool needs plain text
The trap that catches nearly everyone: the file must be created by the service account on the job machine. Create it as yourself and it decrypts only for you; the scheduled run fails with an unhelpful error at two in the morning. Do the one-time creation in a runas session as the account, on the machine the job runs on, and re-create it when the job moves machines.
Windows Credential Manager — the per-user credential vault — is the same idea with a system-maintained container: entries created while running as the service account are available to that account's sessions and can be read at runtime by scripts or by tools that integrate with it. Same binding, same trap: create entries as the account, on the machine. Its limitation is tooling; if your transfer client cannot read the vault, the DPAPI file pattern above does the same job with two lines of PowerShell.
The last resort: a plaintext file with a tight ACL, for tools that can only read a config file. It is honest to call this what it is — a plaintext secret whose entire protection is filesystem permissions and the shortness of the list of admins and backup readers. Apply the icacls pattern above, keep the file out of every repository and sync folder, and prefer any of the earlier options where the tooling allows.
Cron-Side Homes for Job Secrets
The Unix answer is simpler because the platform's habit — small files, strict modes, per-user homes — is already the right pattern. The key file with 600 permissions covers SFTP. For passwords, the equivalent is a per-job config file owned by the service user, mode 600, in the account's home or a root-owned config directory — read by the script at runtime, never passed on a command line. A variant worth knowing for multi-job machines: files owned root:svc-transfer with mode 640, so root manages the secrets, the service group reads them, and the service account itself cannot accidentally modify or delete its own credential. Two platform specifics deserve honest notes. .netrc, the traditional per-user login file honored by tools like curl and lftp, is workable under exactly the same rules — it is plaintext, so it must be 600 under a dedicated account, and it centralizes every machine credential in one very attractive file, which is a reason to prefer per-job config files instead. And keep secrets out of the crontab itself: crontab lines are copied into the system log every time they run, and environment assignments there end up wherever the crontab is backed up or printed. The crontab schedules; the job reads its own secret from its own protected file.
The Decision Guide
| Situation | Good home for the secret | Avoid |
|---|---|---|
| Windows job, SFTP | Key file in the service account's protected folder, ACL-restricted | Key sitting beside the script in a shared or synced folder |
| Windows job, password auth (FTPS/FTP, shares) | DPAPI file or Credential Manager entry, created as the service account on the job machine | Passwords in scripts, task arguments, or environment blocks |
| The task's own Windows logon | Task Scheduler's stored credential, or a managed service account where offered | Running the task as a person to dodge the stored password |
| Cron job, SFTP | Per-flow key, mode 600, in the service user's ~/.ssh | One key shared across machines and flows |
| Cron job, password-based tool | Per-job config file (or .netrc), mode 600, read by the script | Passwords in the crontab line or on the command line |
| Any platform, any auth | One documented location per job, named in the jobs register | Repositories, wikis, ticket comments, and anything that syncs |
Rule of thumb: one job, one secret, one documented place. The moment a credential serves two jobs, or exists in two locations "for convenience," rotation stops being safe — because no one can say with confidence what breaks when it changes.
Out of Repositories, Out of Backups
Scripts belong in version control; secrets do not; and the two are magnetically attracted. The working separation is structural: the repository carries a template (partner.cred.example, a config with blank fields) plus ignore rules for the real thing, and the real secret exists only on the job machine, in its protected location. If a real secret does land in version control, deleting the file does not help — history keeps it — so the response is always the same: rotate the credential, then clean up at leisure.
The same magnetism applies to convenience copies. A job folder that lives inside a synced or roaming location — a cloud-sync directory, a redirected profile, a "shared tools" drive — replicates its key files to every device and account the sync reaches, silently and durably. Job directories holding secrets belong on local, unsynced paths, and it is worth checking once per review that no sync client has adopted them since.
Backups need the same honesty. A backup of the job folder contains the key file; a system-state backup of a Windows machine contains its stored secrets. That is not a reason to skip backups — it is a reason to count backup readers into the secret's audience, protect backup storage accordingly, and prefer designs where what the backup captures is useless elsewhere (DPAPI's machine binding quietly gives you this: the restored file decrypts only on the original machine for the original account). Finally, documentation: the jobs register from scheduled job hygiene should record where each job's secret lives and when it last changed — never the secret itself.
Rotation That Does Not Break the Schedule
Credentials change: policy demands it, people who knew them leave, exposure is suspected. For an unattended job, an unplanned change is an outage — the job cannot ask anyone for the new password — so rotation has to be a small ceremony instead of an edit:
- Open a change window and tell whoever owns the flow's other end.
- Add the new credential alongside the old wherever possible, rather than replacing it. Key authentication makes this natural: the server holds both public keys during the overlap. Password systems often allow a parallel account for the same purpose.
- Update the job's stored secret — re-export the DPAPI file, replace the key file, update the vault entry. If the service account's own Windows password rotated, update the task's stored logon too (
schtasks /change /rp, or re-enter it in the task's properties). - Run the job once by hand and verify end to end — connection, transfer, log lines on both sides.
- Watch the next scheduled run; unattended is the only test that fully counts.
- Revoke the old credential, and watch for anything that starts failing — that is how you discover the undocumented second job using the same secret.
- Update the register: date, who rotated, where the secret lives now.
Monitoring belongs on both ends of this ceremony. A job whose credential silently went stale announces itself as repeated authentication failures — the signature that monitoring authentication attacks teaches you to catch, whether the cause is an attacker or last week's rotation. On the server side, retiring a credential is exactly when logs earn their keep: if the far end is your Sysax Multi Server, its activity logging to file or database shows any login still attempting the old identity, which turns "did everything move over?" into a query. And when a person with knowledge of job secrets departs, rotation is not optional — the checklist in retiring keys at offboarding covers the job-credential sweep.
It is also fair to count consolidation as a strategy. Part of why job credentials sprawl is that every script carries its own connection details. Where the scheduled work is transfers, a tool like Sysax FTP Automation keeps the connection details for its scheduled transfer tasks in connection profiles inside one product — so a partner's rotated password is one profile update, not a hunt through scripts, and the task's built-in retry and failure email make a missed rotation loudly visible at the next run instead of quietly weeks later.
The Shape of Done
A job with its credentials handled properly looks like this: authentication by key where the protocol allows; every stored secret readable by exactly the service account plus the administrators you have consciously accepted; nothing in scripts, arguments, crontabs, repositories, or wikis; the storage location and last-rotation date written in the register; and a rotation runbook that has been exercised at least once on purpose. Build the identity first in service accounts for scheduled jobs, wire the scheduler in Task Scheduler done properly, and record all of it per scheduled job hygiene — the three articles this one leans on.
Frequently Asked Questions
Is it safe to put a password in an environment variable?
Should an unattended job's SSH key have a passphrase?
Who can read the password Task Scheduler stores for a task?
Why does my script's Import-Clixml credential fail only when scheduled?
A password was committed to our repo — is deleting the file enough?
From the Sysax team: we build secure file transfer software for Windows — Sysax Multi Server, an FTP, FTPS, SFTP, and HTTPS server, and Sysax FTP Automation for scheduled, scripted transfers. Free trials are on the download page.
