|  | @@ -33,6 +33,8 @@ import org.elasticsearch.common.settings.KeyStoreWrapper;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.settings.SecureString;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.settings.Settings;
 | 
	
		
			
				|  |  |  import org.elasticsearch.core.CheckedConsumer;
 | 
	
		
			
				|  |  | +import org.elasticsearch.core.Nullable;
 | 
	
		
			
				|  |  | +import org.elasticsearch.core.PathUtils;
 | 
	
		
			
				|  |  |  import org.elasticsearch.core.SuppressForbidden;
 | 
	
		
			
				|  |  |  import org.elasticsearch.discovery.DiscoveryModule;
 | 
	
		
			
				|  |  |  import org.elasticsearch.discovery.SettingsBasedSeedHostsProvider;
 | 
	
	
		
			
				|  | @@ -59,10 +61,12 @@ import java.nio.file.LinkOption;
 | 
	
		
			
				|  |  |  import java.nio.file.Path;
 | 
	
		
			
				|  |  |  import java.nio.file.StandardCopyOption;
 | 
	
		
			
				|  |  |  import java.nio.file.StandardOpenOption;
 | 
	
		
			
				|  |  | +import java.nio.file.attribute.GroupPrincipal;
 | 
	
		
			
				|  |  |  import java.nio.file.attribute.PosixFileAttributeView;
 | 
	
		
			
				|  |  |  import java.nio.file.attribute.PosixFilePermission;
 | 
	
		
			
				|  |  |  import java.nio.file.attribute.PosixFilePermissions;
 | 
	
		
			
				|  |  |  import java.nio.file.attribute.UserPrincipal;
 | 
	
		
			
				|  |  | +import java.nio.file.attribute.UserPrincipalLookupService;
 | 
	
		
			
				|  |  |  import java.security.KeyPair;
 | 
	
		
			
				|  |  |  import java.security.KeyStore;
 | 
	
		
			
				|  |  |  import java.security.PrivateKey;
 | 
	
	
		
			
				|  | @@ -110,6 +114,7 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
 | 
	
		
			
				|  |  |      private static final String TRANSPORT_AUTOGENERATED_KEYSTORE_NAME = "transport";
 | 
	
		
			
				|  |  |      private static final String TRANSPORT_KEY_KEYSTORE_ENTRY = "transport";
 | 
	
		
			
				|  |  |      private static final String TRANSPORT_CA_CERT_KEYSTORE_ENTRY = "transport_ca";
 | 
	
		
			
				|  |  | +    private static final String ELASTICSEARCH_GROUP_OWNER = "elasticsearch";
 | 
	
		
			
				|  |  |      private static final int TRANSPORT_CERTIFICATE_DAYS = 99 * 365;
 | 
	
		
			
				|  |  |      private static final int TRANSPORT_CA_CERTIFICATE_DAYS = 99 * 365;
 | 
	
		
			
				|  |  |      private static final int TRANSPORT_KEY_SIZE = 4096;
 | 
	
	
		
			
				|  | @@ -152,8 +157,9 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
 | 
	
		
			
				|  |  |      @Override
 | 
	
		
			
				|  |  |      protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
 | 
	
		
			
				|  |  |          final boolean inEnrollmentMode = options.has(enrollmentTokenParam);
 | 
	
		
			
				|  |  | +        final boolean inReconfigureMode = options.has(reconfigure);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        // skipping security auto configuration because node considered as restarting.
 | 
	
		
			
				|  |  | +        // skipping security auto-configuration because node considered as restarting.
 | 
	
		
			
				|  |  |          for (Path dataPath : env.dataFiles()) {
 | 
	
		
			
				|  |  |              if (Files.isDirectory(dataPath) && false == isDirEmpty(dataPath)) {
 | 
	
		
			
				|  |  |                  final String msg = "Skipping security auto configuration because it appears that the node is not starting up for the "
 | 
	
	
		
			
				|  | @@ -197,11 +203,11 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
 | 
	
		
			
				|  |  |              notifyOfFailure(inEnrollmentMode, terminal, Terminal.Verbosity.NORMAL, ExitCodes.NOOP, msg);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        if (options.has(reconfigure)) {
 | 
	
		
			
				|  |  | +        if (inReconfigureMode) {
 | 
	
		
			
				|  |  |              if (false == inEnrollmentMode) {
 | 
	
		
			
				|  |  |                  throw new UserException(ExitCodes.USAGE, "enrollment-token is a mandatory parameter when reconfiguring the node");
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  | -            env = possibleReconfigureNode(env, terminal);
 | 
	
		
			
				|  |  | +            env = possiblyReconfigureNode(env, terminal);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          // only perform auto-configuration if the existing configuration is not conflicting (eg Security already enabled)
 | 
	
	
		
			
				|  | @@ -458,13 +464,21 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              // the HTTP CA PEM file is provided "just in case". The node doesn't use it, but clients (configured manually, outside of the
 | 
	
		
			
				|  |  |              // enrollment process) might indeed need it, and it is currently impossible to retrieve it
 | 
	
		
			
				|  |  | -            fullyWriteFile(tempGeneratedTlsCertsDir, HTTP_AUTOGENERATED_CA_NAME + ".crt", false, stream -> {
 | 
	
		
			
				|  |  | -                try (
 | 
	
		
			
				|  |  | -                    JcaPEMWriter pemWriter = new JcaPEMWriter(new BufferedWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8)))
 | 
	
		
			
				|  |  | -                ) {
 | 
	
		
			
				|  |  | -                    pemWriter.writeObject(httpCaCert);
 | 
	
		
			
				|  |  | +            fullyWriteFile(
 | 
	
		
			
				|  |  | +                tempGeneratedTlsCertsDir,
 | 
	
		
			
				|  |  | +                HTTP_AUTOGENERATED_CA_NAME + ".crt",
 | 
	
		
			
				|  |  | +                false,
 | 
	
		
			
				|  |  | +                inReconfigureMode ? ELASTICSEARCH_GROUP_OWNER : null,
 | 
	
		
			
				|  |  | +                stream -> {
 | 
	
		
			
				|  |  | +                    try (
 | 
	
		
			
				|  |  | +                        JcaPEMWriter pemWriter = new JcaPEMWriter(
 | 
	
		
			
				|  |  | +                            new BufferedWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8))
 | 
	
		
			
				|  |  | +                        )
 | 
	
		
			
				|  |  | +                    ) {
 | 
	
		
			
				|  |  | +                        pemWriter.writeObject(httpCaCert);
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  | -            });
 | 
	
		
			
				|  |  | +            );
 | 
	
		
			
				|  |  |          } catch (Throwable t) {
 | 
	
		
			
				|  |  |              try {
 | 
	
		
			
				|  |  |                  deleteDirectory(tempGeneratedTlsCertsDir);
 | 
	
	
		
			
				|  | @@ -528,6 +542,7 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
 | 
	
		
			
				|  |  |                      tempGeneratedTlsCertsDir,
 | 
	
		
			
				|  |  |                      TRANSPORT_AUTOGENERATED_KEYSTORE_NAME + ".p12",
 | 
	
		
			
				|  |  |                      false,
 | 
	
		
			
				|  |  | +                    inReconfigureMode ? ELASTICSEARCH_GROUP_OWNER : null,
 | 
	
		
			
				|  |  |                      stream -> transportKeystore.store(stream, transportKeystorePassword.getChars())
 | 
	
		
			
				|  |  |                  );
 | 
	
		
			
				|  |  |                  nodeKeystore.setString("xpack.security.transport.ssl.keystore.secure_password", transportKeystorePassword.getChars());
 | 
	
	
		
			
				|  | @@ -555,6 +570,7 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
 | 
	
		
			
				|  |  |                      tempGeneratedTlsCertsDir,
 | 
	
		
			
				|  |  |                      HTTP_AUTOGENERATED_KEYSTORE_NAME + ".p12",
 | 
	
		
			
				|  |  |                      false,
 | 
	
		
			
				|  |  | +                    inReconfigureMode ? ELASTICSEARCH_GROUP_OWNER : null,
 | 
	
		
			
				|  |  |                      stream -> httpKeystore.store(stream, httpKeystorePassword.getChars())
 | 
	
		
			
				|  |  |                  );
 | 
	
		
			
				|  |  |                  nodeKeystore.setString("xpack.security.http.ssl.keystore.secure_password", httpKeystorePassword.getChars());
 | 
	
	
		
			
				|  | @@ -838,7 +854,7 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    private Environment possibleReconfigureNode(Environment env, Terminal terminal) throws UserException {
 | 
	
		
			
				|  |  | +    private Environment possiblyReconfigureNode(Environment env, Terminal terminal) throws UserException {
 | 
	
		
			
				|  |  |          // We remove the existing auto-configuration stanza from elasticsearch.yml, the elastisearch.keystore and
 | 
	
		
			
				|  |  |          // the directory with the auto-configured TLS key material, and then proceed as if elasticsearch is started
 | 
	
		
			
				|  |  |          // with --enrolment-token token, in the first place.
 | 
	
	
		
			
				|  | @@ -1044,8 +1060,13 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
 | 
	
		
			
				|  |  |              || ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.get(settings).equals(List.of(NODE_NAME_SETTING.get(settings)));
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    private static void fullyWriteFile(Path basePath, String fileName, boolean replace, CheckedConsumer<OutputStream, Exception> writer)
 | 
	
		
			
				|  |  | -        throws Exception {
 | 
	
		
			
				|  |  | +    private static void fullyWriteFile(
 | 
	
		
			
				|  |  | +        Path basePath,
 | 
	
		
			
				|  |  | +        String fileName,
 | 
	
		
			
				|  |  | +        boolean replace,
 | 
	
		
			
				|  |  | +        @Nullable String groupOwner,
 | 
	
		
			
				|  |  | +        CheckedConsumer<OutputStream, Exception> writer
 | 
	
		
			
				|  |  | +    ) throws Exception {
 | 
	
		
			
				|  |  |          Path filePath = basePath.resolve(fileName);
 | 
	
		
			
				|  |  |          if (false == replace && Files.exists(filePath)) {
 | 
	
		
			
				|  |  |              throw new UserException(
 | 
	
	
		
			
				|  | @@ -1071,17 +1092,28 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
 | 
	
		
			
				|  |  |              PosixFileAttributeView view = Files.getFileAttributeView(tmpPath, PosixFileAttributeView.class);
 | 
	
		
			
				|  |  |              if (view != null) {
 | 
	
		
			
				|  |  |                  view.setPermissions(permission);
 | 
	
		
			
				|  |  | +                if (null != groupOwner) {
 | 
	
		
			
				|  |  | +                    UserPrincipalLookupService lookupService = PathUtils.getDefaultFileSystem().getUserPrincipalLookupService();
 | 
	
		
			
				|  |  | +                    GroupPrincipal groupPrincipal = lookupService.lookupPrincipalByGroupName(groupOwner);
 | 
	
		
			
				|  |  | +                    view.setGroup(groupPrincipal);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              if (replace) {
 | 
	
		
			
				|  |  |                  Files.move(tmpPath, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
 | 
	
		
			
				|  |  |              } else {
 | 
	
		
			
				|  |  |                  Files.move(tmpPath, filePath, StandardCopyOption.ATOMIC_MOVE);
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          } finally {
 | 
	
		
			
				|  |  |              Files.deleteIfExists(tmpPath);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    private static void fullyWriteFile(Path basePath, String fileName, boolean replace, CheckedConsumer<OutputStream, Exception> writer)
 | 
	
		
			
				|  |  | +        throws Exception {
 | 
	
		
			
				|  |  | +        fullyWriteFile(basePath, fileName, replace, null, writer);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      private static boolean isDirEmpty(Path path) throws IOException {
 | 
	
		
			
				|  |  |          // Files.list MUST always be used in a try-with-resource construct in order to release the dir file handler
 | 
	
		
			
				|  |  |          try (Stream<Path> dirContentsStream = Files.list(path)) {
 |