diff --git a/README.md b/README.md index f8de0d6..0c54166 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,24 @@ $ gh-auth add --users=chrishunt --command="tmux attach" Adding 2 key(s) to '/Users/chris/.ssh/authorized_keys' ``` +Allowing someone to SSH to your machine can be a scary thing. Restricting them +to a single command goes a little way to help secure things. If you want to +keep them from forwarding ports, you can use the `--ssh-options` option to +add that to their key entry. + +```bash +$ gh-auth add --users=chrishunt --ssh-options=no-port-forwarding +Adding 2 key(s) to '/Users/chris/.ssh/authorized_keys' +``` + +As a convenience, you may also use the `--no-forwarding` option to prevent all +forwarding (i.e. `no-port-forwarding,no-X11-forwarding,no-agent-forwarding`). + +```bash +$ gh-auth add --users=chrishunt --no-forwarding +Adding 2 key(s) to '/Users/chris/.ssh/authorized_keys' +``` + That was easy! When we're done working, you can revoke my access with: ```bash diff --git a/lib/github/auth/cli.rb b/lib/github/auth/cli.rb index c0a1cca..b19224e 100644 --- a/lib/github/auth/cli.rb +++ b/lib/github/auth/cli.rb @@ -8,6 +8,8 @@ class CLI < Thor option :users, type: :array, required: true option :command, type: :string + option :ssh_options, type: :array + option :no_forwarding, type: :boolean desc 'add', 'Add GitHub users to authorized keys' long_desc <<-LONGDESC `gh-auth add` is used to add one or more GitHub user's public SSH keys @@ -22,11 +24,23 @@ class CLI < Thor the `--command` option. > $ gh-auth add --users=chrishunt --command="tmux attach" + + You may also provide options for the key entry, such as preventing + port forwarding. To do this, include the `--ssh-options` option. + + > $ gh-auth add --users=chrishunt --ssh-options=no-port-forwarding + + To disable all forwarding for their connection, use the + `--no-forwarding` option. + + > $ gh-auth add --users=chrishunt --no-forwarding LONGDESC def add on_keys_file :write!, "Adding #{keys.count} key(s) to '#{keys_file.path}'", - { command: options[:command] } + { command: options[:command], + ssh_options: options[:ssh_options], + no_forwarding: options[:no_forwarding] } end option :users, type: :array, required: true diff --git a/lib/github/auth/keys_file.rb b/lib/github/auth/keys_file.rb index a450468..fc10309 100644 --- a/lib/github/auth/keys_file.rb +++ b/lib/github/auth/keys_file.rb @@ -1,7 +1,13 @@ module Github::Auth # Write and delete keys from the authorized_keys file class KeysFile - attr_reader :path, :command + NO_FORWARDING_OPTIONS = %w( + no-port-forwarding + no-X11-forwarding + no-agent-forwarding + ) + + attr_reader :path, :command, :ssh_options PermissionDeniedError = Class.new StandardError FileDoesNotExistError = Class.new StandardError @@ -10,7 +16,12 @@ class KeysFile def initialize(options = {}) @path = File.expand_path(options[:path] || DEFAULT_PATH) - @command = options[:command] + @command = options[:command] + @ssh_options = Array(options[:ssh_options]) + + if options[:no_forwarding] + @ssh_options |= NO_FORWARDING_OPTIONS + end end def write!(keys) @@ -20,7 +31,7 @@ def write!(keys) unless keys_file_content.empty? || keys_file_content.end_with?("\n") keys_file.write "\n" end - keys_file.write "#{"command=\"#{command}\" " if command}#{key}\n" + keys_file.write key_line(key) end end end @@ -69,5 +80,19 @@ def keys_file_content_without(keys) content << "\n" unless content.empty? || content.end_with?("\n") end end + + def key_line_prefixes + prefixes = [] + prefixes << ssh_options.join(',') unless ssh_options.empty? + prefixes << %Q{command="#{command}"} if command + prefixes.join(',') + end + + def key_line(key) + line = [] + line << key_line_prefixes unless key_line_prefixes.empty? + line << "#{key}\n" + line.join(' ') + end end end diff --git a/spec/acceptance/github/auth/cli_spec.rb b/spec/acceptance/github/auth/cli_spec.rb index 889af5e..d64942a 100644 --- a/spec/acceptance/github/auth/cli_spec.rb +++ b/spec/acceptance/github/auth/cli_spec.rb @@ -54,6 +54,31 @@ def cli(args = []) expect(keys_file.read.strip).to be_empty end + it 'supports ssh options' do + cli %w(add --users=chrishunt + --ssh-options=no-port-forwarding no-agent-forwarding) + + expect(keys_file.read).to include 'no-port-forwarding,no-agent-forwarding' + + keys_file.rewind + cli %w(remove --users=chrishunt) + + expect(keys_file.read.strip).to be_empty + end + + it 'supports no-forwarding option' do + comma_separated_no_forwarding_options = + Github::Auth::KeysFile::NO_FORWARDING_OPTIONS.join(',') + + cli %w(add --users=chrishunt --no-forwarding) + + expect(keys_file.read).to include comma_separated_no_forwarding_options + + keys_file.rewind + cli %w(remove --users=chrishunt) + + end + it 'prints version information' do output = capture_stdout do cli %w(version) diff --git a/spec/unit/github/auth/keys_file_spec.rb b/spec/unit/github/auth/keys_file_spec.rb index c6d046c..a71aea0 100644 --- a/spec/unit/github/auth/keys_file_spec.rb +++ b/spec/unit/github/auth/keys_file_spec.rb @@ -92,6 +92,35 @@ end end + context 'with ssh options' do + let(:options) {{ path: path, ssh_options: ['no-way', 'no-how'] }} + let(:keys) {[ Github::Auth::Key.new('chris', 'abc123') ]} + + it_should_behave_like 'a successful key addition' + + it 'prefixes the key with the comma-separated ssh options' do + subject.write! keys + + expect(keys_file.read).to include 'no-way,no-how' + end + end + + context 'with forwarding disabled' do + let(:options) {{ path: path, no_forwarding: true }} + let(:keys) {[ Github::Auth::Key.new('chris', 'abc123') ]} + + it_should_behave_like 'a successful key addition' + + it 'prefixes the key with all the options to disable forwarding' do + comma_separated_no_forwarding_options = + Github::Auth::KeysFile::NO_FORWARDING_OPTIONS.join(',') + + subject.write! keys + + expect(keys_file.read).to include comma_separated_no_forwarding_options + end + end + context 'with existing keys in the keys file' do let(:existing_keys) { %w(abc123 def456 ghi789) } let(:keys) {[ Github::Auth::Key.new('chris', 'jkl012') ]}