mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-13 12:02:20 +00:00
[JENKINS-73597] Remove RekeySecretAdminMonitor and related resources (#9594)
This commit is contained in:
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());
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user