Add --passfile so credentials stay off the command line
-scp/-dcp put passwords on argv, where any local user can read them with ps. This was not theoretical: a running migration was observed exposing both an AS/400 and a Postgres password in plaintext. -sc/-dc aliases already avoided that, but only read ~/.jrunnerpass, and the pipekit service account is created with --no-create-home, so it had no way to use them. --passfile points at an arbitrary path. Also warn when the passfile is world-readable, and report the resolved path in errors instead of a hardcoded ~/.jrunnerpass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2ced7810d9
commit
e3f624f4d8
@ -4,6 +4,7 @@ import java.util.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path ;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.time.*;
|
||||
import java.io.IOException;
|
||||
import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy;
|
||||
@ -40,6 +41,7 @@ public class jrunner {
|
||||
String nl = "\n";
|
||||
Boolean queryMode = false;
|
||||
String outputFormat = "csv";
|
||||
String passFilePath = "";
|
||||
String msg = "";
|
||||
Connection scon = null;
|
||||
Connection dcon = null;
|
||||
@ -67,9 +69,13 @@ public class jrunner {
|
||||
msg = msg + nl + "-c clear target table";
|
||||
msg = msg + nl + "-b bulk copy into destination (SQL Server dest only)";
|
||||
msg = msg + nl + "-f output format (csv, tsv, table, json) - default: csv";
|
||||
msg = msg + nl + "--passfile path to the connection alias file - default: ~/.jrunnerpass";
|
||||
msg = msg + nl + "--help info";
|
||||
msg = msg + nl + "";
|
||||
msg = msg + nl + "~/.jrunnerpass format:";
|
||||
msg = msg + nl + "Prefer -sc/-dc aliases over -scp/-dcp: a password given on the";
|
||||
msg = msg + nl + "command line is visible to any local user via 'ps'.";
|
||||
msg = msg + nl + "";
|
||||
msg = msg + nl + "passfile format:";
|
||||
msg = msg + nl + " [alias]";
|
||||
msg = msg + nl + " url=jdbc:...";
|
||||
msg = msg + nl + " user=username";
|
||||
@ -140,6 +146,10 @@ public class jrunner {
|
||||
case "-f":
|
||||
outputFormat = args[i+1].toLowerCase();
|
||||
break;
|
||||
//alias file location (the service user has no home directory)
|
||||
case "--passfile":
|
||||
passFilePath = args[i+1];
|
||||
break;
|
||||
case "-v":
|
||||
System.out.println(msg);
|
||||
return;
|
||||
@ -163,13 +173,16 @@ public class jrunner {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve connection aliases from ~/.jrunnerpass
|
||||
// Resolve connection aliases from the passfile
|
||||
if (!scAlias.isEmpty() || !dcAlias.isEmpty()) {
|
||||
Map<String, String[]> connections = loadPassFile();
|
||||
Path resolvedPassFile = passFilePath.isEmpty()
|
||||
? Paths.get(System.getProperty("user.home"), ".jrunnerpass")
|
||||
: Paths.get(passFilePath);
|
||||
Map<String, String[]> connections = loadPassFile(resolvedPassFile);
|
||||
if (!scAlias.isEmpty()) {
|
||||
String[] sc = connections.get(scAlias);
|
||||
if (sc == null) {
|
||||
System.err.println("Error: source alias '" + scAlias + "' not found in ~/.jrunnerpass");
|
||||
System.err.println("Error: source alias '" + scAlias + "' not found in " + resolvedPassFile);
|
||||
System.exit(1);
|
||||
}
|
||||
if (scu.isEmpty()) scu = sc[0];
|
||||
@ -179,7 +192,7 @@ public class jrunner {
|
||||
if (!dcAlias.isEmpty()) {
|
||||
String[] dc = connections.get(dcAlias);
|
||||
if (dc == null) {
|
||||
System.err.println("Error: destination alias '" + dcAlias + "' not found in ~/.jrunnerpass");
|
||||
System.err.println("Error: destination alias '" + dcAlias + "' not found in " + resolvedPassFile);
|
||||
System.exit(1);
|
||||
}
|
||||
if (dcu.isEmpty()) dcu = dc[0];
|
||||
@ -758,19 +771,22 @@ public class jrunner {
|
||||
return value.replaceAll("\t", " ").replaceAll("\n", " ").replaceAll("\r", " ");
|
||||
}
|
||||
|
||||
// Loads ~/.jrunnerpass and returns a map of alias -> {url, user, pass}
|
||||
// Loads a passfile and returns a map of alias -> {url, user, pass}
|
||||
// Format:
|
||||
// [alias]
|
||||
// url=jdbc:...
|
||||
// user=username
|
||||
// pass=password
|
||||
private static Map<String, String[]> loadPassFile() {
|
||||
//
|
||||
// Defaults to ~/.jrunnerpass; --passfile overrides it, which is what lets a
|
||||
// service account with no home directory keep its passwords off argv.
|
||||
private static Map<String, String[]> loadPassFile(Path passFile) {
|
||||
Map<String, String[]> connections = new LinkedHashMap<>();
|
||||
Path passFile = Paths.get(System.getProperty("user.home"), ".jrunnerpass");
|
||||
if (!Files.exists(passFile)) {
|
||||
System.err.println("Error: ~/.jrunnerpass not found");
|
||||
System.err.println("Error: passfile not found: " + passFile);
|
||||
System.exit(1);
|
||||
}
|
||||
warnIfGroupOrWorldReadable(passFile);
|
||||
try {
|
||||
String currentAlias = null;
|
||||
String url = "", user = "", pass = "";
|
||||
@ -795,9 +811,23 @@ public class jrunner {
|
||||
connections.put(currentAlias, new String[]{url, user, pass});
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Error reading ~/.jrunnerpass: " + e.getMessage());
|
||||
System.err.println("Error reading passfile " + passFile + ": " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
// The passfile holds plaintext passwords, so a too-permissive mode defeats
|
||||
// the point of moving them off argv. Warn rather than refuse — the file may
|
||||
// be deliberately group-readable by a service group.
|
||||
private static void warnIfGroupOrWorldReadable(Path passFile) {
|
||||
try {
|
||||
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(passFile);
|
||||
if (perms.contains(PosixFilePermission.OTHERS_READ)) {
|
||||
System.err.println("Warning: " + passFile + " is world-readable; chmod 600 it");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Non-POSIX filesystem, or permissions unreadable — not worth failing over.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user