[JENKINS-73597] Remove RekeySecretAdminMonitor and related resources (#9594)

This commit is contained in:
Jérôme Pochat authored and GitHub committed 2024-08-15 11:29:01 -07:00
1 parent c5c69abd2d
commit c8eddd3f65
30 files changed
-1029

No files matched your search

@@ -1,213 +0,0 @@
package hudson.util;
import hudson.Functions;
import hudson.Util;
import hudson.model.TaskListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
/**
* Rewrites XML files by looking for Secrets that are stored with the old key and replaces them
* by the new encrypted values.
*
* @author Kohsuke Kawaguchi
*/
public class SecretRewriter {
private final Cipher cipher;
private final SecretKey key;
/**
* How many files have been scanned?
*/
private int count;
/**
* Canonical paths of the directories we are recursing to protect
* against symlink induced cycles.
*/
private Set<String> callstack = new HashSet<>();
public SecretRewriter() throws GeneralSecurityException {
cipher = Secret.getCipher("AES");
key = HistoricalSecrets.getLegacyKey();
}
/** @deprecated SECURITY-376: {@code backupDirectory} is ignored */
@Deprecated
public SecretRewriter(File backupDirectory) throws GeneralSecurityException {
this();
}
private String tryRewrite(String s) throws InvalidKeyException {
if (s.length() < 24)
return s; // Encrypting "" in Secret produces 24-letter characters, so this must be the minimum length
if (!isBase64(s))
return s; // decode throws IOException if the input is not base64, and this is also a very quick way to filter
byte[] in;
try {
in = Base64.getDecoder().decode(s.getBytes(StandardCharsets.UTF_8));
} catch (IllegalArgumentException e) {
return s; // not a valid base64
}
cipher.init(Cipher.DECRYPT_MODE, key);
Secret sec = HistoricalSecrets.tryDecrypt(cipher, in);
if (sec != null) // matched
return sec.getEncryptedValue(); // replace by the new encrypted value
else // not encrypted with the legacy key. leave it unmodified
return s;
}
/** @deprecated SECURITY-376: {@code backup} is ignored */
@Deprecated
public boolean rewrite(File f, File backup) throws InvalidKeyException, IOException {
return rewrite(f);
}
public boolean rewrite(File f) throws InvalidKeyException, IOException {
AtomicFileWriter w = new AtomicFileWriter(f.toPath(), StandardCharsets.UTF_8);
try {
boolean modified = false; // did we actually change anything?
try (PrintWriter out = new PrintWriter(new BufferedWriter(w));
InputStream fin = Files.newInputStream(Util.fileToPath(f));
BufferedReader r = new BufferedReader(new InputStreamReader(fin, StandardCharsets.UTF_8))) {
String line;
StringBuilder buf = new StringBuilder();
while ((line = r.readLine()) != null) {
int copied = 0;
buf.setLength(0);
while (true) {
int sidx = line.indexOf('>', copied);
if (sidx < 0) break;
int eidx = line.indexOf('<', sidx);
if (eidx < 0) break;
String elementText = line.substring(sidx + 1, eidx);
String replacement = tryRewrite(elementText);
if (!replacement.equals(elementText))
modified = true;
buf.append(line, copied, sidx + 1);
buf.append(replacement);
copied = eidx;
}
buf.append(line.substring(copied));
out.println(buf);
}
}
if (modified) {
w.commit();
}
return modified;
} finally {
w.abort();
}
}
/**
* Recursively scans and rewrites a directory.
*
* This method shouldn't abort just because one file fails to rewrite.
*
* @return
* Number of files that were actually rewritten.
*/
// synchronized to prevent accidental concurrent use. this instance is not thread safe
public synchronized int rewriteRecursive(File dir, TaskListener listener) throws InvalidKeyException {
return rewriteRecursive(dir, "", listener);
}
private int rewriteRecursive(File dir, String relative, TaskListener listener) throws InvalidKeyException {
String canonical;
try {
canonical = dir.toPath().toRealPath().toString();
} catch (IOException | InvalidPathException e) {
canonical = dir.getAbsolutePath(); //
}
if (!callstack.add(canonical)) {
listener.getLogger().println("Cycle detected: " + dir);
return 0;
}
try {
File[] children = dir.listFiles();
if (children == null) return 0;
int rewritten = 0;
for (File child : children) {
String cn = child.getName();
if (cn.endsWith(".xml")) {
if (count++ % 100 == 0)
listener.getLogger().println("Scanning " + child);
try {
if (rewrite(child)) {
listener.getLogger().println("Rewritten " + child);
rewritten++;
}
} catch (IOException e) {
Functions.printStackTrace(e, listener.error("Failed to rewrite " + child));
}
}
if (child.isDirectory()) {
if (!isIgnoredDir(child))
rewritten += rewriteRecursive(child,
relative.isEmpty() ? cn : relative + '/' + cn,
listener);
}
}
return rewritten;
} finally {
callstack.remove(canonical);
}
}
/**
* Decides if this directory is worth visiting or not.
*/
protected boolean isIgnoredDir(File dir) {
// ignoring the workspace and the artifacts directories. Both of them
// are potentially large and they do not store any secrets.
String n = dir.getName();
return n.equals("workspace") || n.equals("artifacts")
|| n.equals("plugins") // no mutable data here
|| n.equals(".") || n.equals("..");
}
private static boolean isBase64(char ch) {
return ch < 128 && IS_BASE64[ch];
}
private static boolean isBase64(String s) {
for (int i = 0; i < s.length(); i++)
if (!isBase64(s.charAt(i)))
return false;
return true;
}
private static final boolean[] IS_BASE64 = new boolean[128];
static {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
for (int i = 0; i < chars.length(); i++)
IS_BASE64[chars.charAt(i)] = true;
}
}
@@ -17,7 +17,6 @@ import java.nio.charset.Charset;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import jenkins.security.RekeySecretAdminMonitor;
/**
* Convenient partial implementation of {@link AdministrativeMonitor} that involves a background "fixing" action
@@ -27,9 +26,6 @@ import jenkins.security.RekeySecretAdminMonitor;
* A subclass defines what that background fixing actually does in {@link #fix(TaskListener)}. The logging output
* from it gets persisted, and this class provides a "/log" view that allows the administrator to monitor its progress.
*
* <p>
* See {@link RekeySecretAdminMonitor} for an example of how to subtype this class.
*
* @author Kohsuke Kawaguchi
*/
public abstract class AsynchronousAdministrativeMonitor extends AdministrativeMonitor {
@@ -1,160 +0,0 @@
package jenkins.security;
import hudson.Extension;
import hudson.Functions;
import hudson.Util;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.TaskListener;
import hudson.util.HttpResponses;
import hudson.util.SecretRewriter;
import hudson.util.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.security.GeneralSecurityException;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.management.AsynchronousAdministrativeMonitor;
import jenkins.model.Jenkins;
import jenkins.util.io.FileBoolean;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Warns the administrator to run {@link SecretRewriter}
*
* @author Kohsuke Kawaguchi
*/
@Extension @Symbol("rekeySecret")
public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor {
/**
* Whether we detected a need to run the rewrite program.
* Once we set it to true, we'll never turn it off.
*
* If the admin decides to dismiss this warning, we use {@link #isEnabled()} for that.
*
* In this way we can correctly differentiate all the different states.
*/
private final FileBoolean needed = state("needed");
/**
* If the scanning process has run to the completion, we set to this true.
*/
private final FileBoolean done = state("done");
/**
* If the rewrite process is scheduled upon the next boot.
*/
private final FileBoolean scanOnBoot = state("scanOnBoot");
@SuppressWarnings("OverridableMethodCallInConstructor") // should have been final
public RekeySecretAdminMonitor() throws IOException {
// if JENKINS_HOME existed <1.497, we need to offer rewrite
// this computation needs to be done and the value be captured,
// since $JENKINS_HOME/config.xml can be saved later before the user has
// actually rewritten XML files.
Jenkins j = Jenkins.get();
if (j.isUpgradedFromBefore(new VersionNumber("1.496.*"))
&& new FileBoolean(new File(j.getRootDir(), "secret.key.not-so-secret")).isOff())
needed.on();
Util.deleteRecursive(new File(getBaseDir(), "backups")); // SECURITY-376: no longer used
}
@Override
public boolean isActivated() {
return needed.isOn();
}
/**
* Indicates that the re-keying has run to the completion.
*/
public boolean isDone() {
return done.isOn();
}
public void setNeeded() {
needed.on();
}
public boolean isScanOnBoot() {
return scanOnBoot.isOn();
}
@Override
public boolean isSecurity() {
return true;
}
@RequirePOST
public HttpResponse doScan(StaplerRequest req) throws IOException, GeneralSecurityException {
if (req.hasParameter("background")) {
start(false);
} else
if (req.hasParameter("schedule")) {
scanOnBoot.on();
} else
if (req.hasParameter("dismiss")) {
disable(true);
} else
throw HttpResponses.error(400, "Invalid request submission: " + req.getParameterMap());
return HttpResponses.redirectViaContextPath("/manage");
}
private FileBoolean state(String name) {
return new FileBoolean(new File(getBaseDir(), name));
}
@Initializer(fatal = false, after = InitMilestone.PLUGINS_STARTED, before = InitMilestone.EXTENSIONS_AUGMENTED)
// as early as possible, but this needs to be late enough that the ConfidentialStore is available
public void scanOnReboot() throws InterruptedException, IOException, GeneralSecurityException {
FileBoolean flag = scanOnBoot;
if (flag.isOn()) {
flag.off();
start(false).join();
// block the boot until the rewrite process is complete
// don't let the failure in RekeyThread block Jenkins boot.
}
}
@Override
public String getDisplayName() {
return Messages.RekeySecretAdminMonitor_DisplayName();
}
/**
* Rewrite log file.
*/
@Override
protected File getLogFile() {
return new File(getBaseDir(), "rekey.log");
}
@Override
protected void fix(TaskListener listener) throws Exception {
LOGGER.info("Initiating a re-keying of secrets. See " + getLogFile());
SecretRewriter rewriter = new SecretRewriter();
try {
PrintStream log = listener.getLogger();
log.println("Started re-keying " + new Date());
int count = rewriter.rewriteRecursive(Jenkins.get().getRootDir(), listener);
log.printf("Completed re-keying %d files on %s%n", count, new Date());
new RekeySecretAdminMonitor().done.on();
LOGGER.info("Secret re-keying completed");
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Fatal failure in re-keying secrets", e);
Functions.printStackTrace(e, listener.error("Fatal failure in rewriting secrets"));
}
}
private static final Logger LOGGER = Logger.getLogger(RekeySecretAdminMonitor.class.getName());
}