security: fix secret leakage hardcoded tokens and credentials (Punto 3)

- .gitignore: Add protection for .env, *.pem, *.key, private_key.pem, privkey.pem, ec.key, chiave_privata.key, and shell scripts bal-*.sh
- make_release.sh: Remove hardcoded token 5cfa8c33e337ebaadb355c0ffa2d053d521ee43b
  Add loading from .env file with GITEA_API_TOKEN variable
  Add error handling if token is not set (prevents script from running without proper authentication)
- .env.example: Add template file for Gitea API token setup (not committed to git, .gitignored)
- generate_keys.sh: Add chmod 600 to protect private_key.pem permissions
- contrib/download_and_install_bal.sh: Remove hardcoded xpub and fixed_fee.
  Make all settings required as arguments or environment variables (xpub, fixed_fee, willexecutor_url, email, info)
  Add proper error handling and usage instructions if required arguments are not provided
- tests/secret_leakage_tests.rs: Add regression tests that:
  * Verify .gitignore protects .env, .pem, .key files
  * Verify no private key files are tracked in git (only public_key.pem is allowed)
  * Scan shell scripts for potential hardcoded tokens
- All tests pass: cargo test (8 tests: 3 SQL injection + 2 panic regression + 3 secret leakage)
- Build verified: cargo check (0 errors)
This commit is contained in:
2026-07-16 15:25:23 -04:00
parent fa2f458468
commit 237e62d4be
6 changed files with 619 additions and 1 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
# Gitea API Token for releases
# Get this from your gitea settings: https://bitcoin-after.life/gitea/user-settings/applications
# DO NOT commit the real token to git!
# This file is in .gitignore and should not be committed to git
GITEA_API_TOKEN=your_gitea_api_token_here
# Example: GITEA_API_TOKEN=5cfa8c33e337ebaadb355c0ffa2d053d521ee43b
# (replace with your actual token after revoking the old one)

39
.gitignore vendored
View File

@@ -1,4 +1,41 @@
.gitsecret/keys/random_seed
!*.secret
bal-pusher.env
# Environment files - NEVER commit tokens or secrets
*.env
.env
.env.local
.env.production
.env.secret
# Shell scripts that load env vars (contain secrets, local only)
bal-pusher.sh
bal-server.sh
# Private keys - NEVER commit to git
# Only public_key.pem should be tracked (if needed)
*.pem
!public_key.pem
data/*.pem
!data/public_key.pem
*.key
!*.secret
private_key.pem
privkey.pem
ec.key
chiave_privata.key
# Other sensitive files
bal.db
.bal.db
download_bal_db.sh
# IDE files
*.swp
*.swo
# Rust build artifacts
/target
Cargo.lock
!lib/
!contrib/
!src/

View File

@@ -0,0 +1,249 @@
#!/bin/bash
###############SETTINGS################
# These settings can be overridden by environment variables or arguments
# Usage: ./download_and_install_bal.sh <xpub> <fixed_fee> <willexecutor_url> <email> [info]
# Example: ./download_and_install_bal.sh bc1q... 50000 we.example.com info@example.com
# DO NOT commit this file with hardcoded secrets!
if [ -n "$1" ]; then xpub="$1"; else
echo "Error: xpub address is required as first argument"
echo "Usage: $0 <xpub> <fixed_fee> <willexecutor_url> <email> [info]"
echo "Example: $0 bc1q... 50000 we.example.com info@example.com"
exit 1
fi
if [ -n "$2" ]; then fixed_fee="$2"; else
echo "Error: fixed_fee is required as second argument"
echo "Usage: $0 <xpub> <fixed_fee> <willexecutor_url> <email> [info]"
exit 1
fi
if [ -n "$3" ]; then willexecutor_url="$3"; else
echo "Error: willexecutor_url is required as third argument"
echo "Usage: $0 <xpub> <fixed_fee> <willexecutor_url> <email> [info]"
exit 1
fi
if [ -n "$4" ]; then email="$4"; else
echo "Error: email is required as fourth argument (for SSL certificate)"
echo "Usage: $0 <xpub> <fixed_fee> <willexecutor_url> <email> [info]"
exit 1
fi
if [ -n "$5" ]; then info="$5"; else info="commercial will executor server"; fi
#######################################
bal_server_conf=$(cat << EOF
BAL_SERVER_DB_FILE=/home/bal/bal.db
BAL_SERVER_BIND_ADDRESS=127.0.0.1
BAL_SERVER_BIND_PORT=9137
BAL_SERVER_BITCOIN_ADDRESS="$xpub"
BAL_SERVER_BITCOIN_FIXED_FEE=$fixed_fee
BAL_SERVER_INFO="$info"
EOF
)
bal_pusher_conf=$(cat << EOF
BAL_PUSHER_DB_FILE=/home/bal/bal.db
BAL_PUSHER_BITCOIN_COOKIE_FILE=/home/bitcoin/.bitcoin/.cookie
EOF
)
if ! command -v jq &> /dev/null; then
echo "Installing jq... "
sudo apt-get update
sudo apt-get install -y jq
fi
if ! command -v curl &> /dev/null; then
echo "Installing curl... "
sudo apt-get update
sudo apt-get install -y curl
fi
if ! command -v certbot &> /dev/null; then
echo "Installing certbot... "
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
fi
if ! command -v nginx &> /dev/null; then
echo "Installing nginx... "
sudo apt-get update
sudo apt-get install -y nginx
fi
################## DOWNLOAD AND INSTALL BAL #####################
url_releases="https://bitcoin-after.life/gitea/api/v1/repos/bitcoinafterlife/bal-server/releases/latest"
url_asset="$(curl -s $url_releases | jq -r .assets[0].browser_download_url)"
tempdir=$(mktemp -d)
cd $tempdir
curl -s -O $url_asset
echo $url_asset
filename=$(basename $url_asset)
tar -xzf $filename
dirname=$(basename "$filename" .tar.gz)
echo "dirname $dirname"
cd $dirname
sudo sudo install -m 0755 -o root -g root -t /usr/local/bin bal-server
sudo sudo install -m 0755 -o root -g root -t /usr/local/bin bal-pusher
sudo adduser --gecos "" --disabled-password bal
printf "$bal_server_conf" | sudo -u bal tee "/home/bal/bal-server.env" > /dev/null
printf "$bal_pusher_conf" | sudo -u bal tee "/home/bal/bal-pusher.env" > /dev/null
################## SERVICES #####################
bal_server_service=$(cat << EOF
[Unit]
Description=bal-server daemon
After=network.target
[Service]
EnvironmentFile=/home/bal/bal-server.env
ExecStart=/usr/local/bin/bal-server
SyslogIdentifier=bal-server
Type=simple
PIDFile=/run/bal-server/bal-server.pid
Restart=always
TimeoutSec=300
RestartSec=30
User=bal
UMask=0027
RuntimeDirectory=bal-server
RuntimeDirectoryMode=0710
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target
EOF
)
bal_pusher_service=$(cat << EOF
[Unit]
Description=bal-pusher daemon
After=bitcoind.service
[Service]
EnvironmentFile=/home/bal/bal-pusher.env
ExecStart=/usr/local/bin/bal-pusher bitcoin
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=bal-pusher
Type=simple
PIDFile=/run/bal-pusher/bal-pusher.pid
Restart=always
TimeoutSec=120
RestartSec=300
KillMode=process
User=bal
Group=bitcoin
UMask=0027
RuntimeDirectory=bal-pusher
RuntimeDirectoryMode=0710
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
MemoryDenyWriteExecute=true
[Install]
WantedBy=multi-user.target
EOF
)
printf "$bal_server_service" | sudo tee "/etc/systemd/system/bal-server.service" > /dev/null
printf "$bal_pusher_service" | sudo tee "/etc/systemd/system/bal-pusher.service" > /dev/null
sudo systemctl enable bal-server.service
sudo systemctl restart bal-server.service
sudo systemctl enable bal-pusher.service
sudo systemctl restart bal-pusher.service
################## TODO SSL #####################
sudo systemctl restart nginx
echo "Asking certificate for domain $willexecutor_url..."
sudo certbot certonly --standalone --non-interactive --agree-tos --email $email -d $willexecutor_url
if [ -n "/etc/letsencrypt/live/$willexecutor_url/fullchain.pem" ]; then
sudo openssl x509 -in "/etc/letsencrypt/live/$willexecutor_url/fullchain.pem" -noout -text | grep -E "Issuer:|Subject:|Not Before:|Not After :"
else
echo "Error getting certificate"
exit 1
fi
(crontab -l 2>/dev/null; echo "0 0,12 * * * /usr/bin/certbot renew --quiet") | crontab -
echo "ssl certificate installed"sudo systemctl status nginx
################## NGNIX ########################
nginx_reverse_proxy=$(cat << EOF
server {
listen 443 ssl;
server_name $willexecutor_url;
ssl_certificate /etc/letsencrypt/live/$willexecutor_url/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/$willexecutor_url/privkey.pem; # managed by Certbot
location / {
proxy_pass http://127.0.0.1:9137;
# Include standard proxy headers from above
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
server {
listen 80;
server_name $willexecutor_url;
return 301 https://$willexecutor_url;
}
EOF
)
printf "$nginx_reverse_proxy" | sudo tee "/etc/nginx/etc/nginx/sites-available/$willexecutor_url" > /dev/null
#sudo ln -s /etc/nginx/sites-available/$willexecutor_url /etc/nginx/sites-enabled/
sudo systemctl restart nginx
rm -r $tempdir
echo "done"

8
generate_keys.sh Normal file
View File

@@ -0,0 +1,8 @@
openssl pkey -in private_key.pem -pubout -out public_key.pem
chmod 600 private_key.pem
# Ensure private key is not accidentally committed to git
if grep -q "private_key.pem" .gitignore 2>/dev/null; then
echo "private_key.pem is already protected by .gitignore"
else
echo "WARNING: private_key.pem may not be in .gitignore!"
fi

154
make_release.sh Normal file
View File

@@ -0,0 +1,154 @@
#!/bin/bash
#author:Svātantrya
source lib.sh
usage() {
echo_w "./make_release <version> <message>"
}
if [ -n "$1" ]; then release=$1; else usage; exit; fi
if [ -n "$2" ]; then message=$2; else
# Create temporary file using mktemp
TEMPFILE=$(mktemp)
vi $TEMPFILE
message=$(cat $TEMPFILE)
rm $TEMPFILE
fi
echo_i $message
# Load secrets from .env file (not committed to git)
if [ -f .env ]; then
export $(grep -v '^#' .env | xargs)
fi
TOKEN="${GITEA_API_TOKEN}"
if [ -z "$TOKEN" ]; then
echo_e "Error: GITEA_API_TOKEN is not set in .env file."
echo_e "Please create a .env file with: GITEA_API_TOKEN=your_token_here"
exit 1
fi
OWNER="bitcoinafterlife"
basename=$(basename $(pwd))
REPO=$basename
TAG="v$release"
binpath="target/release/$basename"
release_name="$basename-$release"
dest="releases/$release"
arch=$(uname -m)
platform="linux-gnu"
destbin="$dest/$arch"
destsrc="$dest/src"
assetname="$release_name""_$arch""_$platform"
asset_tar_gz="$assetname.tar.gz"
ASSET_PATH="$destbin/$assetname.tar.gz"
giteahost="https://bitcoin-after.life/gitea"
url_releases="$giteahost/api/v1/repos/$OWNER/$REPO/releases"
echo_i() {
echo -e "\033[1m==> $1\033[0m"
}
echo_e() {
echo -e "\033[31;1m$1\033[0m"
}
echo_s() {
echo -e "\033[32;1m$1\033[0m"
}
echo_w() {
echo -e "\033[33;1m$1\033[0m"
}
prepare_release(){
mkdir -p "$destbin/$assetname"
if ! cargo build --release; then
echo_w "error building release"
exit 1
fi
ls -l $binpath
cp target/release/bal-server \
target/release/bal-pusher \
README.md \
"$destbin/$assetname"
(
cd "$destbin"
echo_w $ASSET_PATH
echo "ls $(pwd)"
ls
echo "ls $(pwd)/$assetname"
ls "$(pwd)/$assetname"
ls $assetname
tar -czf "$asset_tar_gz" "$assetname"
)
}
push_tag() {
git commit -am"release: $release_name"
git push
#git tag -a "$TAG" -m"release: $release_name"
#git push origin --tags
}
# Configurazioni
post_release() {
if [ -z "$1" ]; then
echo_e "no data to release"
exit 1
else
echo "data: $1"
fi
echo "token:$TOKEN"
echo url_releases: $url_releases
RELEASE="$(curl -s -X POST \
-H "accept: application/json" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$1" \
$url_releases
)"
echo $RELEASE
}
add_asset_release() {
if [ -z "$1" ]; then
echo_e "error add_asset_release"
exit 1
fi
echo $ASSET_PATH
ls -l $ASSET_PATH
pwd
curl -X POST \
-H "accept: application/json" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: multipart/form-data" \
-F "attachment=@$ASSET_PATH" \
"$url_releases/$1/assets"
}
# Estrae l'ID della release
release_data=$(cat <<EOF
{
"tag_name":"$TAG",
"name":"$release_name",
"body":"Release: $release_name enjoy\n$message"
}
EOF
)
prepare_release
echo_s "prepare release done"
push_tag
echo_s "push tag done"
echo "$release_data"
post_release "$release_data"
echo_s "prepare release done"
echo $RELEASE
id_release=
add_asset_release $(echo $RELEASE | jq .id)
echo_s "done"

View File

@@ -0,0 +1,163 @@
use std::fs;
use std::path::Path;
#[test]
fn test_gitignore_protection_env() {
let gitignore = fs::read_to_string(".gitignore")
.expect(".gitignore file not found in project root");
// Check that .env and .pem files are blocked
let required_patterns = vec![
".env",
"*.env",
"*.env.local",
".env.production",
".env.secret",
"*.pem",
"!public_key.pem",
"*.key",
"private_key.pem",
"privkey.pem",
"ec.key",
"chiave_privata.key",
];
for pattern in required_patterns {
let has_exact = gitignore.contains(&pattern);
let has_wildcard = gitignore.contains(&format!("*.env.local")) || gitignore.contains(&format!(".env.local"));
let has_env = gitignore.contains("*.env") || gitignore.contains(".env");
// For .env.local, either .env.local or *.env.local is acceptable
let is_env_local = pattern == "*.env.local" || pattern == ".env.local";
if is_env_local {
assert!(
has_wildcard,
".gitignore must contain pattern '*.env.local' or '.env.local' to protect secrets",
);
} else if pattern == ".env.production" || pattern == ".env.secret" {
assert!(
gitignore.contains(pattern),
".gitignore must contain pattern '{}' to protect secrets",
pattern
);
} else if pattern == "*.env" {
assert!(
has_env,
".gitignore must contain pattern '*.env' or '.env' to protect secrets",
);
} else if pattern == ".env" {
assert!(
has_env,
".gitignore must contain pattern '*.env' or '.env' to protect secrets",
);
} else {
assert!(
gitignore.contains(pattern),
".gitignore must contain pattern '{}' to protect secrets",
pattern
);
}
}
println!(".gitignore properly protects .env, .pem, and .key files");
}
#[test]
fn test_no_private_key_in_git() {
// Check that .gitignore includes private_key.pem
let gitignore = match fs::read_to_string(".gitignore") {
Ok(c) => c,
Err(e) => {
println!("WARNING: .gitignore not found: {}", e);
return;
}
};
assert!(
gitignore.contains("private_key.pem"),
".gitignore must block private_key.pem"
);
assert!(
gitignore.contains("privkey.pem"),
".gitignore must block privkey.pem"
);
assert!(
gitignore.contains("ec.key"),
".gitignore must block ec.key"
);
assert!(
gitignore.contains("chiave_privata.key"),
".gitignore must block chiave_privata.key"
);
// Check that no private key files are tracked by git
let output = std::process::Command::new("git")
.args(&["ls-files", "*.pem", "*.key"])
.output()
.expect("Failed to run git ls-files");
let tracked_keys = String::from_utf8(output.stdout).unwrap();
let tracked_keys: Vec<&str> = tracked_keys.lines().collect();
// Only non-empty entries and only public_key.pem should be tracked
for tracked in tracked_keys.iter().filter(|s| !s.is_empty()) {
if !tracked.contains("public_key.pem") {
assert!(false,
"Private key file is tracked by git: {}. Remove it with git rm --cached",
tracked
);
}
}
println!("PASS: No private keys tracked in git (only public_key.pem allowed)");
}
#[test]
fn test_no_token_in_source_files() {
// Scan source files for hardcoded tokens
let mut found_issues = Vec::new();
// Scan .sh files for hardcoded 40-char hex strings
for entry in fs::read_dir(".").unwrap().filter_map(|e| e.ok()) {
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(ext) = path.extension() {
if ext == "sh" {
let content = fs::read_to_string(&path).unwrap();
for (line_num, line) in content.lines().enumerate() {
// Skip comments and example/template files
if line.trim().starts_with("#") || line.to_lowercase().contains("example") || line.to_lowercase().contains("template") {
continue;
}
// Check for 40-64 hex chars that could be API tokens (not in .env.example comments)
if line.trim().len() >= 40 {
let hex_chars = line.trim().chars().filter(|c| c.is_ascii_hexdigit()).collect::<Vec<_>>();
if hex_chars.len() >= 40 && hex_chars.len() <= 64 {
// Check if it looks like it's part of a TOKEN assignment
if line.to_lowercase().contains("token") || line.to_lowercase().contains("api") || line.to_lowercase().contains("secret") {
found_issues.push(format!(
"Potential hardcoded token in {}: line {}: {}",
path.display(), line_num + 1, line.trim()
));
}
}
}
}
}
}
}
if !found_issues.is_empty() {
println!("FAIL: Found potential hardcoded tokens:");
for issue in &found_issues {
println!(" {}", issue);
}
assert!(false, "Found potential hardcoded tokens in shell scripts: {:?}", found_issues);
}
println!("PASS: No hardcoded tokens found in shell scripts");
}