Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nodes reload request store password as SecureString #31261

Conversation

albertzaharovits
Copy link
Contributor

This improvement was suggested by @jaymode in a comment in another review.

The node request, NodesReloadSecureSettingsRequest, broadcasting the password required to locally decrypt the keystore on each node, stores the password in a SecureString request field and not a plain String field.

Relates: #29135
CC @elastic/es-security

@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-core-infra

final byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
return bytes;
}
Copy link
Contributor Author

@albertzaharovits albertzaharovits Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied from:

Alternatively, we could pull the CharArrays utils class out of x-pack. Yet this is the only use case in server. Moreover, CharArrays brings a methods that is narrowed to the security scope, namely constantTimeEquals.
Another alternative would be to have the utf8BytesToChars and toUtf8Bytes as static members of SecureString since right now uses are tied to SecureString.

All in all, I think copying them here for this sole use is the best alternative.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another alternative would be to have the utf8BytesToChars and toUtf8Bytes as static members of SecureString since right now uses are tied to SecureString.

I actually think this might be the best option but we should do this separately in a followup.

final CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer);
final char[] chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit());
Arrays.fill(charBuffer.array(), (char) 0); // clear sensitive data
return chars;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied from:

See previous comment .

Some point worth raising here. In the original implementation (CharArray#utf8BytesToChars) the decoded CharBuffer is not cleared. It is charBuffer.clear() but should be Arrays.fill(charBuffer.array(), (char) 0);.
@jaymode Should I raise a PR to fix this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is worth raising a separate PR to fix this and move the methods as I said before. I also think that this method needs to handle some other cases such as my suggestion above about the hasArray() and other items.

Copy link
Member

@jaymode jaymode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some ideas for improvements. Also I wonder if it is possible to clear the secureSettingsPassword after the request has been sent to all the nodes?

final byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
return bytes;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another alternative would be to have the utf8BytesToChars and toUtf8Bytes as static members of SecureString since right now uses are tied to SecureString.

I actually think this might be the best option but we should do this separately in a followup.

private static byte[] toUtf8Bytes(char[] chars) {
final CharBuffer charBuffer = CharBuffer.wrap(chars);
final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
final byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is dangerous so I think we should correct it. The char buffer is guaranteed to be a HeapCharBuffer by the javadocs; however there is no such guarantee about the ByteBuffer returned by StandardCharsets.UTF_8.encode(charBuffer). We need to account for whether the bytebuffer has an array that can be accessed or not.

final byte[] bytes;
if (byteBuffer.hasArray()) {
    // there is no guarantee that the byte buffers backing array is the right size so we need to make a copy
    bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
    Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
} else {
    final int length = byteBuffer.limit() - byteBuffer.position();
    bytes = new byte[length];
    byteBuffer.get(bytes);
    // if the buffer is not read only we can reset and fill with 0's
    if (byteBuffer.isReadOnly() == false) {
         byteBuffer.clear(); // reset
         for (int i = 0; i < byteBuffer.limit(); i++) {
             byteBuffer.put((byte) 0);
         }
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed snippet.

final CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer);
final char[] chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit());
Arrays.fill(charBuffer.array(), (char) 0); // clear sensitive data
return chars;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is worth raising a separate PR to fix this and move the methods as I said before. I also think that this method needs to handle some other cases such as my suggestion above about the hasArray() and other items.

@albertzaharovits
Copy link
Contributor Author

Thank you for the feedback @jaymode .
I will open a follow-up PR to move the NodesReloadSecureSettingsRequest#charsToUtf8Bytes and NodeesReloadSecureSettingsRequest#utf8BytesToChars to SecureString after the feature branch is merged in.

There are two points worth bringing up after addressing the feedback, which I have pointed in the code.

}
return this;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1/2: Since right now the keystore only uses the empty password, the previous implementation was sloppy and actually read the password as a REST parameter. This is terrible and has been corrected. It is now read from the request body.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to need to figure out a way to filter this in the security auditing. In x-pack we introduced the concept of a RestRequestFilter. This concept might need to be promoted to core for this API.

@@ -82,15 +83,15 @@ protected NodeRequest newNodeRequest(String nodeId, NodesReloadSecureSettingsReq
protected NodesReloadSecureSettingsResponse.NodeResponse nodeOperation(NodeRequest nodeReloadRequest) {
final NodesReloadSecureSettingsRequest request = nodeReloadRequest.request;
KeyStoreWrapper keystore = null;
try {
try (final SecureString secureSettingsPassword = request.secureSettingsPassword()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2/2 (1): The password is cleared on each NodeRequest.

@@ -68,7 +72,8 @@ public RestResponse buildResponse(NodesReloadSecureSettingsResponse response, XC
builder.field("cluster_name", response.getClusterName().value());
response.toXContent(builder, channel.request());
builder.endObject();

// clear password for the original request
nodesRequest.secureSettingsPassword().close();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2/2 (2): The password is also cleared on the original NodesRequest.
The way everything fits together is as follows: The NodeSRequest has the password which has been read from the REST body. The password is stored as a SecureString. It is passed as a reference to all the constructed NodeRequests.

    public static class NodeRequest extends BaseNodeRequest {

        NodesReloadSecureSettingsRequest request;

        public NodeRequest() {
        }

        NodeRequest(String nodeId, NodesReloadSecureSettingsRequest request) {
            super(nodeId);
            this.request = request;
        }

        @Override
        public void readFrom(StreamInput in) throws IOException {
            super.readFrom(in);
            request = new NodesReloadSecureSettingsRequest();
            request.readFrom(in);
        }

        @Override
        public void writeTo(StreamOutput out) throws IOException {
            super.writeTo(out);
            request.writeTo(out);
        }
    }

ALL these requests are serialized (including the one for the local node, doing the broadcast) and their password is not cleared. The deserialized NodeRequests will have their password cleared: https://github.com/elastic/elasticsearch/pull/31261/files#r195506031 .
The original password (read from the REST body) which was passed as a reference to all the broadcasted requests, which have been serialized, is cleared here.

All goes down in flames if the password in the NodeRequest for the local node is not copied (through serialize-deserialize) AND it is cleared before other requests are serialized.
Since ALL NodeRequests are serialized this should not happen, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe everything you've said is correct

Copy link
Member

@jaymode jaymode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -68,7 +72,8 @@ public RestResponse buildResponse(NodesReloadSecureSettingsResponse response, XC
builder.field("cluster_name", response.getClusterName().value());
response.toXContent(builder, channel.request());
builder.endObject();

// clear password for the original request
nodesRequest.secureSettingsPassword().close();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe everything you've said is correct

}
return this;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to need to figure out a way to filter this in the security auditing. In x-pack we introduced the concept of a RestRequestFilter. This concept might need to be promoted to core for this API.

@albertzaharovits albertzaharovits merged commit d9835d5 into elastic:reload-secure-store-action Jun 16, 2018
@albertzaharovits albertzaharovits deleted the secure-string-request branch June 16, 2018 19:26
albertzaharovits added a commit that referenced this pull request Jun 16, 2018
@bellatrix1001
Copy link

@jaymode How does one actually set this password via POST? Docs have no reference to secure_settings_password. Thanks!!

@jaymode
Copy link
Member

jaymode commented Nov 9, 2018

Password has been removed in a separate issue since we didn't have full support for it, so you cannot set it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
:Core/Infra/Settings Settings infrastructure and APIs >non-issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants