add-build-sshkey: Remove only the master key
This implements a module to directly interact with the ssh-agent so that the master key may be removed from the ssh-agent without removing any per-project keys. Change-Id: Ife91ad8afa9b41b0e779a832e298aca8d61ae98b
This commit is contained in:
parent
ce56d5182a
commit
46389b5187
@ -3,9 +3,10 @@ Generate and install a build-local SSH key on all hosts
|
||||
This role is intended to be run on the Zuul Executor at the start of
|
||||
every job. It generates an SSH keypair and installs the public key in
|
||||
the authorized_keys file of every host in the inventory. It then
|
||||
removes all keys from this job's SSH agent so that the original key
|
||||
used to log into all of the hosts is no longer accessible, then adds
|
||||
the newly generated private key.
|
||||
removes the Zuul master key from this job's SSH agent so that the
|
||||
original key used to log into all of the hosts is no longer accessible
|
||||
(any per-project keys, if present, remain available), then adds the
|
||||
newly generated private key.
|
||||
|
||||
**Role Variables**
|
||||
|
||||
|
0
roles/add-build-sshkey/__init__.py
Normal file
0
roles/add-build-sshkey/__init__.py
Normal file
0
roles/add-build-sshkey/library/__init__.py
Normal file
0
roles/add-build-sshkey/library/__init__.py
Normal file
126
roles/add-build-sshkey/library/sshagent_remove_keys.py
Normal file
126
roles/add-build-sshkey/library/sshagent_remove_keys.py
Normal file
@ -0,0 +1,126 @@
|
||||
# Copyright 2018 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
SSH_AGENT_FAILURE = 5
|
||||
SSH_AGENT_SUCCESS = 6
|
||||
SSH_AGENT_IDENTITIES_ANSWER = 12
|
||||
|
||||
SSH_AGENTC_REQUEST_IDENTITIES = 11
|
||||
SSH_AGENTC_REMOVE_IDENTITY = 18
|
||||
|
||||
|
||||
def unpack_string(data):
|
||||
(l,) = struct.unpack('!i', data[:4])
|
||||
d = data[4:4 + l]
|
||||
return (d, data[4 + l:])
|
||||
|
||||
|
||||
def pack_string(data):
|
||||
ret = struct.pack('!i', len(data))
|
||||
return ret + data
|
||||
|
||||
|
||||
class Agent(object):
|
||||
def __init__(self):
|
||||
path = os.environ['SSH_AUTH_SOCK']
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.sock.connect(path)
|
||||
|
||||
def send(self, message_type, contents):
|
||||
payload = struct.pack('!ib', len(contents) + 1, message_type)
|
||||
payload += bytearray(contents)
|
||||
self.sock.send(payload)
|
||||
|
||||
def recv(self):
|
||||
buf = b''
|
||||
while len(buf) < 5:
|
||||
buf += self.sock.recv(1)
|
||||
message_len, message_type = struct.unpack('!ib', buf[:5])
|
||||
buf = buf[5:]
|
||||
while len(buf) < message_len - 1:
|
||||
buf += self.sock.recv(1)
|
||||
return message_type, buf
|
||||
|
||||
def list(self):
|
||||
self.send(SSH_AGENTC_REQUEST_IDENTITIES, b'')
|
||||
mtype, data = self.recv()
|
||||
if mtype != SSH_AGENT_IDENTITIES_ANSWER:
|
||||
raise Exception("Invalid response to list")
|
||||
(nkeys,) = struct.unpack('!i', data[:4])
|
||||
data = data[4:]
|
||||
keys = []
|
||||
for i in range(nkeys):
|
||||
blob, data = unpack_string(data)
|
||||
comment, data = unpack_string(data)
|
||||
keys.append((blob, comment))
|
||||
return keys
|
||||
|
||||
def remove(self, blob):
|
||||
self.send(SSH_AGENTC_REMOVE_IDENTITY, pack_string(blob))
|
||||
mtype, data = self.recv()
|
||||
if mtype != SSH_AGENT_SUCCESS:
|
||||
raise Exception("Key was not removed")
|
||||
|
||||
|
||||
def run(remove):
|
||||
a = Agent()
|
||||
keys = a.list()
|
||||
removed = []
|
||||
to_remove = re.compile(remove)
|
||||
for blob, comment in keys:
|
||||
if not to_remove.match(comment.decode('utf8')):
|
||||
continue
|
||||
a.remove(blob)
|
||||
removed.append(comment)
|
||||
return removed
|
||||
|
||||
|
||||
def ansible_main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
remove=dict(required=True, type='str')))
|
||||
|
||||
removed = run(module.params.get('remove'))
|
||||
|
||||
module.exit_json(changed=(removed != []),
|
||||
removed=removed)
|
||||
|
||||
|
||||
def cli_main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Remove ssh keys from agent"
|
||||
)
|
||||
parser.add_argument('remove', nargs='+',
|
||||
help='regex matching comments of keys to remove')
|
||||
args = parser.parse_args()
|
||||
|
||||
removed = run(args.remove)
|
||||
print(removed)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if sys.stdin.isatty():
|
||||
cli_main()
|
||||
else:
|
||||
ansible_main()
|
85
roles/add-build-sshkey/library/test_sshagent_remove_keys.py
Normal file
85
roles/add-build-sshkey/library/test_sshagent_remove_keys.py
Normal file
@ -0,0 +1,85 @@
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
#
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import testtools
|
||||
import fixtures
|
||||
import subprocess
|
||||
import paramiko
|
||||
import signal
|
||||
|
||||
from .sshagent_remove_keys import Agent, run
|
||||
|
||||
|
||||
class AgentFixture(fixtures.Fixture):
|
||||
|
||||
def _setUp(self):
|
||||
self.env = {}
|
||||
with open('/dev/null', 'r+') as devnull:
|
||||
ssh_agent = subprocess.Popen(['ssh-agent'], close_fds=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=devnull,
|
||||
stdin=devnull)
|
||||
(output, _) = ssh_agent.communicate()
|
||||
output = output.decode('utf8')
|
||||
for line in output.split("\n"):
|
||||
if '=' in line:
|
||||
line = line.split(";", 1)[0]
|
||||
(key, value) = line.split('=')
|
||||
self.env[key] = value
|
||||
os.environ[key] = value
|
||||
self.addCleanup(self.stop)
|
||||
|
||||
def stop(self):
|
||||
if 'SSH_AGENT_PID' in self.env:
|
||||
os.kill(int(self.env['SSH_AGENT_PID']), signal.SIGTERM)
|
||||
|
||||
|
||||
class TestAgent(testtools.TestCase):
|
||||
def test_agent(self):
|
||||
"""Test the ssh agent library"""
|
||||
self.useFixture(AgentFixture())
|
||||
tmpdir = fixtures.TempDir()
|
||||
self.useFixture(tmpdir)
|
||||
|
||||
k1_path = os.path.join(tmpdir.path, 'key1')
|
||||
k1 = paramiko.RSAKey.generate(bits=1024)
|
||||
k1.write_private_key_file(k1_path)
|
||||
self.assertTrue(os.path.exists(k1_path))
|
||||
subprocess.call(['ssh-add', k1_path])
|
||||
|
||||
k2_path = os.path.join(tmpdir.path, 'key2')
|
||||
k2 = paramiko.RSAKey.generate(bits=1024)
|
||||
k2.write_private_key_file(k2_path)
|
||||
self.assertTrue(os.path.exists(k2_path))
|
||||
with open(k2_path, 'r') as f:
|
||||
k2_private = f.read()
|
||||
proc = subprocess.Popen(['ssh-add', '-'],
|
||||
stdin=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
proc.communicate(input=k2_private.encode('utf8'))
|
||||
|
||||
a = Agent()
|
||||
l = a.list()
|
||||
self.assertEqual(2, len(l))
|
||||
|
||||
run('^(?!\(stdin\)).*')
|
||||
|
||||
l = a.list()
|
||||
self.assertEqual(1, len(l))
|
||||
self.assertTrue(b'stdin' in l[0][1])
|
@ -29,8 +29,11 @@
|
||||
mode: 0644
|
||||
force: no
|
||||
|
||||
- name: Remove all keys from local agent
|
||||
command: ssh-add -D
|
||||
- name: Remove master key from local agent
|
||||
# The master key has a filename, all others (e.g., per-project keys)
|
||||
# have "(stdin)" as a comment.
|
||||
sshagent_remove_keys:
|
||||
remove: '^(?!\(stdin\)).*'
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
|
2
tox.ini
2
tox.ini
@ -51,6 +51,6 @@ commands = {posargs}
|
||||
# These are ignored intentionally in openstack-infra projects;
|
||||
# please don't submit patches that solely correct them or enable them.
|
||||
# E402 - ansible modules put documentation before imports. Align to ansible.
|
||||
ignore = E125,E129,E402,H
|
||||
ignore = E125,E129,E402,E741,H
|
||||
show-source = True
|
||||
exclude = .venv,.tox,dist,doc,build,*.egg
|
||||
|
Loading…
Reference in New Issue
Block a user