package jenkins.security; import hudson.Extension; 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 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.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.interceptor.RequirePOST; 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; /** * Warns the administrator to run {@link SecretRewriter} * * @author Kohsuke Kawaguchi */ @Extension @Symbol("rekeySecret") public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor implements StaplerProxy { /** * 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"); 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.getInstance(); if (j.isUpgradedFromBefore(new VersionNumber("1.496.*")) && new FileBoolean(new File(j.getRootDir(),"secret.key.not-so-secret")).isOff()) needed.on(); } /** * Requires ADMINISTER permission for any operation in here. */ public Object getTarget() { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); return this; } @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(); } @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(new File(getBaseDir(),"backups")); try { PrintStream log = listener.getLogger(); log.println("Started re-keying " + new Date()); int count = rewriter.rewriteRecursive(Jenkins.getInstance().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); e.printStackTrace(listener.error("Fatal failure in rewriting secrets")); } } private static final Logger LOGGER = Logger.getLogger(RekeySecretAdminMonitor.class.getName()); }