-
Notifications
You must be signed in to change notification settings - Fork 483
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
feat: add support for ssh property in the build command #1058
Merged
p12tic
merged 1 commit into
containers:main
from
banditopazzo:705-ssh-key-support-in-build
Oct 15, 2024
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Added support for "ssh" property in the build command. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Base image | ||
FROM alpine:latest | ||
|
||
# Install OpenSSH client | ||
RUN apk add openssh | ||
|
||
# Test the SSH agents during the build | ||
|
||
RUN echo -n "default: " >> /result.log | ||
RUN --mount=type=ssh ssh-add -L >> /result.log | ||
|
||
RUN echo -n "id1: " >> /result.log | ||
RUN --mount=type=ssh,id=id1 ssh-add -L >> /result.log | ||
|
||
RUN echo -n "id2: " >> /result.log | ||
RUN --mount=type=ssh,id=id2 ssh-add -L >> /result.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
version: "3" | ||
services: | ||
test_build_ssh_map: | ||
build: | ||
context: ./context | ||
dockerfile: Dockerfile | ||
ssh: | ||
default: | ||
id1: "./id_ed25519_dummy" | ||
id2: "./agent_dummy.sock" | ||
image: my-alpine-build-ssh-map | ||
command: | ||
- cat | ||
- /result.log | ||
test_build_ssh_array: | ||
build: | ||
context: ./context | ||
dockerfile: Dockerfile | ||
ssh: | ||
- default | ||
- "id1=./id_ed25519_dummy" | ||
- "id2=./agent_dummy.sock" | ||
image: my-alpine-build-ssh-array | ||
command: | ||
- cat | ||
- /result.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
-----BEGIN OPENSSH PRIVATE KEY----- | ||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW | ||
QyNTUxOQAAACBWELzfWvraCAeo0rOM2OxTGqWZx7fNBCglK/1oS8FLpgAAAJhzHuERcx7h | ||
EQAAAAtzc2gtZWQyNTUxOQAAACBWELzfWvraCAeo0rOM2OxTGqWZx7fNBCglK/1oS8FLpg | ||
AAAEAEIrYvY3jJ2IvAnUa5jIrVe8UG+7G7PzWzZqqBQykZllYQvN9a+toIB6jSs4zY7FMa | ||
pZnHt80EKCUr/WhLwUumAAAADnJpbmdvQGJuZHRib3gyAQIDBAUGBw== | ||
-----END OPENSSH PRIVATE KEY----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
# SPDX-License-Identifier: GPL-2.0 | ||
|
||
import os | ||
import socket | ||
import struct | ||
import threading | ||
import unittest | ||
|
||
from cryptography.hazmat.primitives import serialization | ||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey | ||
|
||
from tests.integration.test_podman_compose import podman_compose_path | ||
from tests.integration.test_podman_compose import test_path | ||
from tests.integration.test_utils import RunSubprocessMixin | ||
|
||
expected_lines = [ | ||
"default: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYQvN9a+toIB6jSs4zY7FMapZnHt80EKCUr/WhLwUum", | ||
"id1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYQvN9a+toIB6jSs4zY7FMapZnHt80EKCUr/WhLwUum", | ||
"id2: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYQvN9a+toIB6jSs4zY7FMapZnHt80EKCUr/WhLwUum", | ||
] | ||
|
||
|
||
class TestBuildSsh(unittest.TestCase, RunSubprocessMixin): | ||
def test_build_ssh(self): | ||
"""The build context can contain the ssh authentications that the image builder should | ||
use during image build. They can be either an array or a map. | ||
""" | ||
|
||
compose_path = os.path.join(test_path(), "build_ssh/docker-compose.yml") | ||
sock_path = os.path.join(test_path(), "build_ssh/agent_dummy.sock") | ||
private_key_file = os.path.join(test_path(), "build_ssh/id_ed25519_dummy") | ||
|
||
agent = MockSSHAgent(private_key_file) | ||
|
||
try: | ||
# Set SSH_AUTH_SOCK because `default` expects it | ||
os.environ['SSH_AUTH_SOCK'] = sock_path | ||
|
||
# Start a mock SSH agent server | ||
agent.start_agent(sock_path) | ||
|
||
self.run_subprocess_assert_returncode([ | ||
podman_compose_path(), | ||
"-f", | ||
compose_path, | ||
"build", | ||
"test_build_ssh_map", | ||
"test_build_ssh_array", | ||
]) | ||
|
||
for test_image in [ | ||
"test_build_ssh_map", | ||
"test_build_ssh_array", | ||
]: | ||
out, _ = self.run_subprocess_assert_returncode([ | ||
podman_compose_path(), | ||
"-f", | ||
compose_path, | ||
"run", | ||
"--rm", | ||
test_image, | ||
]) | ||
|
||
out = out.decode('utf-8') | ||
|
||
# Check if all lines are contained in the output | ||
self.assertTrue( | ||
all(line in out for line in expected_lines), | ||
f"Incorrect output for image {test_image}", | ||
) | ||
|
||
finally: | ||
# Now we send the stop command to gracefully shut down the server | ||
agent.stop_agent() | ||
|
||
if os.path.exists(sock_path): | ||
os.remove(sock_path) | ||
|
||
self.run_subprocess_assert_returncode([ | ||
"podman", | ||
"rmi", | ||
"my-alpine-build-ssh-map", | ||
"my-alpine-build-ssh-array", | ||
]) | ||
|
||
|
||
# SSH agent message types | ||
SSH_AGENTC_REQUEST_IDENTITIES = 11 | ||
SSH_AGENT_IDENTITIES_ANSWER = 12 | ||
SSH_AGENT_FAILURE = 5 | ||
STOP_REQUEST = 0xFF | ||
|
||
|
||
class MockSSHAgent: | ||
def __init__(self, private_key_path): | ||
self.sock_path = None | ||
self.server_sock = None | ||
self.running = threading.Event() | ||
self.keys = [self._load_ed25519_private_key(private_key_path)] | ||
self.agent_thread = None # Thread to run the agent | ||
|
||
def _load_ed25519_private_key(self, private_key_path): | ||
"""Load ED25519 private key from an OpenSSH private key file.""" | ||
with open(private_key_path, 'rb') as key_file: | ||
private_key = serialization.load_ssh_private_key(key_file.read(), password=None) | ||
|
||
# Ensure it's an Ed25519 key | ||
if not isinstance(private_key, Ed25519PrivateKey): | ||
raise ValueError("Invalid key type, expected ED25519 private key.") | ||
|
||
# Get the public key corresponding to the private key | ||
public_key = private_key.public_key() | ||
|
||
# Serialize the public key to the OpenSSH format | ||
public_key_blob = public_key.public_bytes( | ||
encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw | ||
) | ||
|
||
# SSH key type "ssh-ed25519" | ||
key_type = b"ssh-ed25519" | ||
|
||
# Build the key blob (public key part for the agent) | ||
key_blob_full = ( | ||
struct.pack(">I", len(key_type)) | ||
+ key_type # Key type length + type | ||
+ struct.pack(">I", len(public_key_blob)) | ||
+ public_key_blob # Public key length + key blob | ||
) | ||
|
||
# Comment (empty) | ||
comment = "" | ||
|
||
return ("ssh-ed25519", key_blob_full, comment) | ||
|
||
def start_agent(self, sock_path): | ||
"""Start the mock SSH agent and create a Unix domain socket.""" | ||
self.sock_path = sock_path | ||
if os.path.exists(self.sock_path): | ||
os.remove(self.sock_path) | ||
|
||
self.server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
self.server_sock.bind(self.sock_path) | ||
self.server_sock.listen(5) | ||
|
||
os.environ['SSH_AUTH_SOCK'] = self.sock_path | ||
|
||
self.running.set() # Set the running event | ||
|
||
# Start a thread to accept client connections | ||
self.agent_thread = threading.Thread(target=self._accept_connections, daemon=True) | ||
self.agent_thread.start() | ||
|
||
def _accept_connections(self): | ||
"""Accept and handle incoming connections.""" | ||
while self.running.is_set(): | ||
try: | ||
client_sock, _ = self.server_sock.accept() | ||
self._handle_client(client_sock) | ||
except Exception as e: | ||
print(f"Error accepting connection: {e}") | ||
|
||
def _handle_client(self, client_sock): | ||
"""Handle a single client request (like ssh-add).""" | ||
try: | ||
# Read the message length (first 4 bytes) | ||
length_message = client_sock.recv(4) | ||
if not length_message: | ||
raise "no length message received" | ||
|
||
msg_len = struct.unpack(">I", length_message)[0] | ||
|
||
request_message = client_sock.recv(msg_len) | ||
|
||
# Check for STOP_REQUEST | ||
if request_message[0] == STOP_REQUEST: | ||
client_sock.close() | ||
self.running.clear() # Stop accepting connections | ||
return | ||
|
||
# Check for SSH_AGENTC_REQUEST_IDENTITIES | ||
if request_message[0] == SSH_AGENTC_REQUEST_IDENTITIES: | ||
response = self._mock_list_keys_response() | ||
client_sock.sendall(response) | ||
else: | ||
print("Message not recognized") | ||
# Send failure if the message type is not recognized | ||
response = struct.pack(">I", 1) + struct.pack(">B", SSH_AGENT_FAILURE) | ||
client_sock.sendall(response) | ||
|
||
except socket.error: | ||
print("Client socket error.") | ||
pass # You can handle specific errors here if needed | ||
finally: | ||
client_sock.close() # Ensure the client socket is closed | ||
|
||
def _mock_list_keys_response(self): | ||
"""Create a mock response for ssh-add -l, listing keys.""" | ||
|
||
# Start building the response | ||
response = struct.pack(">B", SSH_AGENT_IDENTITIES_ANSWER) # Message type | ||
|
||
# Number of keys | ||
response += struct.pack(">I", len(self.keys)) | ||
|
||
# For each key, append key blob and comment | ||
for key_type, key_blob, comment in self.keys: | ||
# Key blob length and content | ||
response += struct.pack(">I", len(key_blob)) + key_blob | ||
|
||
# Comment length and content | ||
comment_encoded = comment.encode() | ||
response += struct.pack(">I", len(comment_encoded)) + comment_encoded | ||
|
||
# Prefix the entire response with the total message length | ||
response = struct.pack(">I", len(response)) + response | ||
|
||
return response | ||
|
||
def stop_agent(self): | ||
"""Stop the mock SSH agent.""" | ||
if self.running.is_set(): # First check if the agent is running | ||
# Create a temporary connection to send the stop command | ||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_sock: | ||
client_sock.connect(self.sock_path) # Connect to the server | ||
|
||
stop_command = struct.pack( | ||
">B", STOP_REQUEST | ||
) # Pack the stop command as a single byte | ||
|
||
# Send the message length first | ||
message_length = struct.pack(">I", len(stop_command)) | ||
client_sock.sendall(message_length) # Send the length first | ||
|
||
client_sock.sendall(stop_command) # Send the stop command | ||
|
||
self.running.clear() # Stop accepting new connections | ||
|
||
# Wait for the agent thread to finish | ||
if self.agent_thread: | ||
self.agent_thread.join() # Wait for the thread to finish | ||
self.agent_thread = None # Reset thread reference | ||
|
||
# Remove the socket file only after the server socket is closed | ||
if self.server_sock: # Check if the server socket exists | ||
self.server_sock.close() # Close the server socket | ||
os.remove(self.sock_path) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 would make sense to produce some output that shows that information was really acquired from the agent and then assert it in the test.
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.
do you mean not having the mock agent at all?
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.
sorry, got it, I found a solution to check the key
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.
Sorry for not being explicit, but your solution was pretty similar to what I was thinking about.
A more simplier solution would be to do:
Then in docker compose set command to
cat /result.log
and then check the logs in the test viapodman compose logs
(there are a few tests that do this to use for reference).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.
Done