Skip to content

Commit e7e2194

Browse files
committed
Implementation of SSH keys for Secret Manager
SSH keys are an integral part of the Secret Manager service, which allows users to manage and deliver them to nodes. Examples: ```bash curl --location 'http://10.20.0.2:11010/v1/secret/ssh_keys/' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer MY_TOKEN' \ --data-raw '{ "name": "my-key", "project_id": "00000000-0000-0000-0000-000000000000", "user": "ubuntu", "target": { "kind": "node", "node": "10000000-1000-1000-1000-000000000001" }, "target_public_key": "ssh-rsa AAAABBBBCCCC user@user-pc" }' ``` When the key is delivered to the target, it is added to the authorized keys ($HOME/.ssh/authorized_keys) of the user on the target. Also it's status is set to `ACTIVE`. Docs: https://github.com/infraguys/genesis_core/wiki/SSHKeys Signed-off-by: Anton Kremenetsky <anton.kremenetsky@gmail.com>
1 parent aa6f563 commit e7e2194

10 files changed

Lines changed: 1073 additions & 2 deletions

File tree

genesis_core/config/service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ def _get_outdated_configs(
8585
order_by={"uuid": "asc"},
8686
)
8787
resources = ua_models.TargetResource.objects.get_all(
88-
filters={"uuid": dm_filters.In(str(cfg) for cfg in config_uuids)},
88+
filters={
89+
"uuid": dm_filters.In(str(cfg) for cfg in config_uuids),
90+
"kind": dm_filters.EQ(cc.CONFIG_KIND),
91+
},
8992
order_by={"uuid": "asc"},
9093
)
9194

genesis_core/secret/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818

1919
PASSWORD_KIND = "password"
2020
CERTIFICATE_KIND = "certificate"
21+
SSH_KEY_KIND = "ssh_key"
22+
SSH_KEY_TARGET_KIND = "ssh_key_target"
23+
24+
AUTHORIZED_KEYS_PATH = ".ssh/authorized_keys"
2125

2226

2327
class SecretStatus(str, enum.Enum):

genesis_core/secret/dm/models.py

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
# under the License.
1616
from __future__ import annotations
1717

18+
import typing as tp
19+
import uuid as sys_uuid
20+
1821
from restalchemy.dm import properties
1922
from restalchemy.dm import types
2023
from restalchemy.dm import types_network
@@ -24,8 +27,9 @@
2427

2528
from gcl_sdk.agents.universal.dm import models as ua_models
2629

27-
from genesis_core.common.dm import models as cm
2830
from genesis_core.common import constants as c
31+
from genesis_core.common.dm import models as cm
32+
from genesis_core.config.dm import models as cfg_models
2933
from genesis_core.secret import constants as sc
3034

3135

@@ -207,3 +211,126 @@ def get_deleted_certificates(
207211
return cls.get_deleted_target_resources(
208212
cls.__tablename__, sc.CERTIFICATE_KIND, limit
209213
)
214+
215+
216+
class SSHKey(
217+
Secret,
218+
orm.SQLStorableMixin,
219+
ua_models.TargetResourceSQLStorableMixin,
220+
):
221+
__tablename__ = "secret_ssh_keys"
222+
223+
target = properties.property(
224+
types_dynamic.KindModelSelectorType(
225+
types_dynamic.KindModelType(cfg_models.NodeTarget),
226+
),
227+
required=True,
228+
)
229+
user = properties.property(types.String(min_length=1, max_length=64))
230+
authorized_keys = properties.property(
231+
types.String(min_length=1, max_length=256),
232+
default=sc.AUTHORIZED_KEYS_PATH,
233+
)
234+
target_public_key = properties.property(
235+
types.String(max_length=10240),
236+
default="",
237+
)
238+
239+
def target_nodes(self) -> tp.List[sys_uuid.UUID]:
240+
return self.target.target_nodes()
241+
242+
def get_resource_target_fields(self) -> set[str]:
243+
"""Return the collection of target fields.
244+
245+
Refer to the Resource model for more details about target fields.
246+
"""
247+
return {
248+
"uuid",
249+
"name",
250+
"description",
251+
"project_id",
252+
"constructor",
253+
"user",
254+
"authorized_keys",
255+
"target",
256+
"target_public_key",
257+
}
258+
259+
def to_host_resource(
260+
self,
261+
master: sys_uuid.UUID,
262+
node: sys_uuid.UUID,
263+
status: sc.SecretStatus | None = None,
264+
) -> ua_models.TargetResource:
265+
"""Create a target resource for a specific host (node).
266+
267+
This creates a 'slave' resource for a specific node, which is linked
268+
to the 'master' SSHKey secret.
269+
270+
Args:
271+
master: The UUID of the master SSHKey secret.
272+
node: The UUID of the target node.
273+
status: The initial status for the host resource.
274+
275+
Returns:
276+
A TargetResource instance for the host.
277+
"""
278+
properties = {}
279+
280+
# Copy properties
281+
for name in self.properties.properties.keys():
282+
if name not in SSHHostKey.properties.properties:
283+
continue
284+
properties[name] = getattr(self, name)
285+
286+
# Correct UUID based on node UUID
287+
properties["uuid"] = sys_uuid.uuid5(self.uuid, str(node))
288+
host_ssh = SSHHostKey(**properties)
289+
290+
resource = host_ssh.to_ua_resource(
291+
sc.SSH_KEY_TARGET_KIND, master=master
292+
)
293+
if status is not None:
294+
resource.status = status.value
295+
# Place the key on the node
296+
resource.agent = node
297+
298+
return resource
299+
300+
@classmethod
301+
def get_new_keys(cls, limit: int = c.DEFAULT_SQL_LIMIT) -> list["SSHKey"]:
302+
303+
return cls.get_new_entities(cls.__tablename__, sc.SSH_KEY_KIND, limit)
304+
305+
@classmethod
306+
def get_updated_keys(
307+
cls, limit: int = c.DEFAULT_SQL_LIMIT
308+
) -> list["SSHKey"]:
309+
return cls.get_updated_entities(
310+
cls.__tablename__, sc.SSH_KEY_KIND, limit
311+
)
312+
313+
@classmethod
314+
def get_deleted_keys(
315+
cls, limit: int = c.DEFAULT_SQL_LIMIT
316+
) -> list[ua_models.TargetResource]:
317+
return cls.get_deleted_target_resources(
318+
cls.__tablename__, sc.SSH_KEY_KIND, limit
319+
)
320+
321+
322+
class SSHHostKey(
323+
ra_models.ModelWithUUID,
324+
ua_models.TargetResourceMixin,
325+
):
326+
"""SSH host key model."""
327+
328+
user = properties.property(types.String(min_length=1, max_length=64))
329+
authorized_keys = properties.property(
330+
types.String(min_length=1, max_length=256),
331+
default=sc.AUTHORIZED_KEYS_PATH,
332+
)
333+
target_public_key = properties.property(
334+
types.String(max_length=10240),
335+
default="",
336+
)

0 commit comments

Comments
 (0)