-
Notifications
You must be signed in to change notification settings - Fork 24.8k
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
Nodes reload request store password as SecureString #31261
Conversation
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copied from:
Line 43 in 113c191
public static byte[] toUtf8Bytes(char[] chars) { |
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copied from:
Line 18 in 113c191
public static char[] utf8BytesToChars(byte[] utf8Bytes) { |
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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; | ||
} |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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);
}
}
}
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
Thank you for the feedback @jaymode . There are two points worth bringing up after addressing the feedback, which I have pointed in the code. |
} | ||
return this; | ||
} | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 NodeRequest
s.
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 NodeRequest
s 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 NodeRequest
s are serialized this should not happen, right?
There was a problem hiding this comment.
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
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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; | ||
} | ||
|
There was a problem hiding this comment.
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.
@jaymode How does one actually set this password via POST? Docs have no reference to secure_settings_password. Thanks!! |
Password has been removed in a separate issue since we didn't have full support for it, so you cannot set it. |
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 aSecureString
request field and not a plainString
field.Relates: #29135
CC @elastic/es-security