How to Encrypt a Shell Script with shc (and Why It Won’t Hide Your Passwords)



A question every DBA hears sooner or later: "This script has the database password in it. Can we encrypt it so people who run it can't read it?" On Linux the usual answer is shc, the shell script compiler. It turns a script into a binary, so cat and vi show nothing useful.

In this post we install shc, compile a script, add an expiry date, and then test the part most guides skip: can another user still see the password? Every output below comes from a real test on shc 4.0.3, Ubuntu 24.04 and bash 5.2.

Short answer: shc hides your script from casual readers, but it is not a safe place for passwords. In our test, any local user could read the full script, password included, from /proc while the binary was running. Section 08 shows what to use instead.

01What shc is (and is not)

shc was written by Francisco Rosales and is now maintained as an open-source project on GitHub (neurobin/shc, GPL-3.0). Despite the name, it is not a real compiler. It works in two steps:

  1. It encrypts your script with the RC4 cipher and embeds it, together with the key, in a generated C program (script.x.c).
  2. It compiles that C program with cc into a binary (script.x).

When you run the binary, it decrypts the script in memory and hands it to the shell named on the first line (#!/bin/bash). So the binary still needs bash on the server, and the decryption key travels inside the binary itself.


02Install shc

The old download site used in many tutorials no longer works. Install it from your distribution instead:

PlatformCommand
Ubuntu / Debiansudo apt install shc
Oracle Linux / RHEL / Rocky (EPEL)sudo dnf install epel-release (on Oracle Linux: oracle-epel-release-el8 or -el9) then sudo dnf install shc
Fedorasudo dnf install shc
From sourcegit clone https://github.com/neurobin/shc.git → ./configure && make && sudo make install

shc also needs a C compiler (gcc) on the machine where you compile. Check the version:

$ as script owner
$ shc -h | head -1
shc Version 4.0.3, Generic Shell Script Compiler

03Compile a script

Our test script does exactly what the question describes: it contains a password. (Never do this for real; it is here only to test shc.)

db_check.sh
#!/bin/bash
# db_check.sh - demo script that (badly) contains a password
DB_USER="report_user"
DB_PASS="S3cr3t#Demo2026"
echo "Connecting as ${DB_USER} ..."
sleep "${1:-0}"
echo "Check complete on $(hostname) at $(date '+%F %T')"
$ as script owner
$ shc -f db_check.sh
$ ls -l db_check.sh*
-rwx------ 1 appuser appuser   230 Oct 28 05:56 db_check.sh
-rwxrwxr-x 1 appuser appuser 15384 Oct 28 05:56 db_check.sh.x
-rw-rw-r-- 1 appuser appuser 18909 Oct 28 05:56 db_check.sh.x.c
FileWhat it is
db_check.shYour original script, unchanged
db_check.sh.xThe compiled binary that you deploy
db_check.sh.x.cGenerated C source with the RC4-encrypted script and its key. Delete it; it is all someone needs to rebuild the binary.
Look at the permissions: the binary was created as rwxrwxr-x (775). Every user on the server can run it and copy it. Fix that before anything else (section 07).
$ as script owner
$ file db_check.sh.x
db_check.sh.x: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, stripped

$ ./db_check.sh.x
Connecting as report_user ...
Check complete on dbhost01 at 2026-10-28 05:56:55

$ strings db_check.sh.x | grep -c S3cr3t
0

So far so good: the binary runs exactly like the script, and strings finds no password. This is where most tutorials stop.

04Set an expiry date

shc can stop a binary from running after a date (-e, format dd/mm/yyyy) and show your own message (-m). It is handy for scripts you hand out for a fixed period, such as a migration window. We used a past date to see the result:

$ as script owner
$ shc -e 31/12/2025 -m "Expired: contact the DBA team for a new version" -f db_check.sh -o db_check_exp
$ ./db_check_exp
./db_check_exp: has expired!
Expired: contact the DBA team for a new version
$ echo $?
1

The check uses the server clock, so it is a convenience, not a licence lock: anyone who can change the system date can get around it.

05shc 4.x options

Several older tutorials use -T to make the binary traceable. That option no longer exists: in shc 4.x binaries are traceable by default, and -U makes them untraceable.

OptionWhat it does
-f fileScript to compile (required)
-o fileOutput file name (default: script.x)
-e dd/mm/yyyyExpiry date
-m "text"Message shown after expiry
-rRelax security: make a redistributable binary for other servers with the same OS (by default it is tied to the shell binary of the build host)
-UUntraceable: refuses to run under strace / gdb
-HHardening: extra protection against dumping and ptrace. Works only with /bin/sh scripts and does not support arguments
-vVerbose: shows the cc and strip commands it runs
-Ssetuid for root-callable programs. Avoid this unless you fully understand the risk
-BCompile for busybox

06The password test: what other users can see

Now the real question. We copied the binary to the home of appuser, ran it there (with a 5-second sleep so it stays alive), and from a different, unprivileged account looked at the process:

$ as appuser
$ ./db_check.sh.x 5 &
$ as otheruser (no special rights)
$ tr '\0' '\n' < /proc/309/cmdline
./db_check.sh.x
-c
 #!/bin/bash
# db_check.sh - demo script that (badly) contains a password
DB_USER="report_user"
DB_PASS="S3cr3t#Demo2026"
echo "Connecting as ${DB_USER} ..."

The whole decrypted script, password included. shc passes the script to bash as a command-line argument (bash -c "<script>"). ps shows only padding, but /proc/<pid>/cmdline is readable by every local user by default. We repeated the test for each build mode:

BuildScript visible in /proc/<pid>/cmdline?
default (shc -f)Yes, full script with password
-U untraceableYes, full script with password
-H hardening (/bin/sh, no arguments)No, cmdline was empty

Even with -H, remember that the RC4 key is inside the binary. Anyone who can copy the binary can study it at leisure, and decoders for older shc versions have been published. shc raises the effort; it does not make a secret safe.

07If you still use shc

shc is fine for its real purpose: keeping the logic of a script away from casual eyes, or shipping a script with an expiry date. If you use it, do it like this:

$ as script owner
shc -H -f job.sh -o job          # use -H when the script is plain sh with no arguments
rm -f job.sh.x.c                  # never leave the generated C source behind
chmod 700 job                     # owner only (711 if others must run it, they still can't copy it)
# keep job.sh in your source repository, not on the server
# as root
# hide other users' processes from each other (test monitoring tools first)
mount -o remount,hidepid=2 /proc
# make it permanent in /etc/fstab:
proc  /proc  proc  defaults,hidepid=2  0 0
Note on hidepid: some monitoring agents and tools expect to see all processes. Test on a non-production server, and on systemd hosts check that services which need it are covered (for example with the gid= mount option).

08Better ways to handle passwords in scripts

The real fix is to keep the password out of the script. Every major database already has a standard place for it, protected by file permissions:

Database / toolStore the password inScript then runs
OracleSecure External Password Store (wallet)sqlplus /@PRODDB
PostgreSQL~/.pgpass (chmod 600)psql -h dbhost -U report_user -d appdb
MySQL / MariaDBmysql_config_editor login pathmysql --login-path=report
Any scriptConfig file owned by a service account, chmod 600sudo -u svc_report /opt/scripts/job.sh
Cloud / enterpriseHashiCorp Vault, Azure Key Vault, AWS Secrets Managerfetch the secret at run time with a machine identity

Example: Oracle wallet

$ as oracle
mkdir -p /u01/app/oracle/wallet
mkstore -wrl /u01/app/oracle/wallet -create
mkstore -wrl /u01/app/oracle/wallet -createCredential PRODDB report_user
# mkstore prompts for the passwords, so they never appear in shell history or ps
$ORACLE_HOME/network/admin/sqlnet.ora
WALLET_LOCATION = (SOURCE = (METHOD = FILE) (METHOD_DATA = (DIRECTORY = /u01/app/oracle/wallet)))
SQLNET.WALLET_OVERRIDE = TRUE
script: no password anywhere
sqlplus -s /@PRODDB <<'EOF'
SELECT name, open_mode FROM v$database;
EOF

Example: PostgreSQL .pgpass

$ as script owner
echo 'dbhost:5432:appdb:report_user:YourStrongPassword' >> ~/.pgpass
chmod 600 ~/.pgpass
psql -h dbhost -U report_user -d appdb -c 'SELECT now();'   # no password prompt
Rule of thumb: protect secrets with permissions and identities (who can read the file, which account runs the job), not by hiding text. Also keep passwords off the command line: arguments are visible in /proc exactly as shown above.

09FAQ

Is shc real encryption?

It uses RC4, but the key is stored in the same binary, and the script is decrypted every time it runs. Treat it as obfuscation, not encryption.

Can I move the binary to another server?

Only if it was built with -r, and the target needs the same OS family and the shell from the first line.

Does shc make my script faster?

No. The binary decrypts the script and runs it with the normal shell, so speed is the same (plus a tiny start-up cost).

Then why use it at all?

To keep business logic away from casual readers, or to give out a script that stops working after a date. For secrets, use a wallet, .pgpass or a vault.

© 2026 Tamilselvan Perumal · TechStarAdmin. Tested with shc 4.0.3 on Ubuntu 24.04 (bash 5.2). The password in this post is a demo value. Please link back instead of copying.
Originally published at techstaradmin.blogspot.com.