From fba17982d5e6f48f66203831f0cd9aea59fb3cc5 Mon Sep 17 00:00:00 2001 From: Tejay Cardon Date: Tue, 1 Sep 2015 12:04:44 -0600 Subject: [PATCH] * add authorized_keys resource --- .kitchen.yml | 3 + .rubocop.yml | 10 +- CHANGELOG.md | 3 + README.md | 98 ++++++++++- libraries/ssh_config_helpers.rb | 4 +- metadata.rb | 2 +- providers/authorized_keys.rb | 157 ++++++++++++++++++ providers/known_hosts.rb | 11 +- recipes/default.rb | 2 +- resources/authorized_keys.rb | 21 +++ .../ssh_test/recipes/authorized_keys.rb | 25 +++ .../serverspec/authorized_keys_spec.rb | 10 ++ .../authorized_keys/serverspec/spec_helper.rb | 3 + 13 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 providers/authorized_keys.rb create mode 100644 resources/authorized_keys.rb create mode 100644 test/cookbooks/ssh_test/recipes/authorized_keys.rb create mode 100644 test/integration/authorized_keys/serverspec/authorized_keys_spec.rb create mode 100644 test/integration/authorized_keys/serverspec/spec_helper.rb diff --git a/.kitchen.yml b/.kitchen.yml index 461b1f0..ded2666 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -29,3 +29,6 @@ suites: - name: config run_list: - recipe[ssh_test::config] + - name: authorized_keys + run_list: + - recipe[ssh_test::authorized_keys] diff --git a/.rubocop.yml b/.rubocop.yml index 4a2f24f..b69465a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,8 @@ +AllCops: + Exclude: + - 'bin/**/*' + - '.kitchen/**/*' + AlignParameters: Enabled: false ClassAndModuleChildren: @@ -16,5 +21,8 @@ MethodLength: Severity: refactor SingleSpaceBeforeFirstArg: Enabled: false +AbcSize: + Enabled: false Documentation: - Enabled: false \ No newline at end of file + Enabled: false + diff --git a/CHANGELOG.md b/CHANGELOG.md index d95689e..c1bbcc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # CHANGELOG for ssh +## 0.10.6 +* add authorized_keys resource + ## 0.10.5 * add support for RHEL family diff --git a/README.md b/README.md index b95068b..bdf3ccb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Description -Provides 2 LWRPs to manage system-wide and per-user `ssh_config` and `known_host` files. +Provides 3 LWRPs to manage system-wide and per-user `ssh_config` and `known_host` files. ## Setup @@ -171,3 +171,99 @@ some of your cookbooks may not be as generous. options 'User' => 'git', 'IdentityFile' => '/var/apps/github_deploy_key' user 'webapp' end + +### authorized_keys +The authorized_keys LWRP is considered _Beta_ due to the lack of tests for this resource. Use at your own risk, +and feel free to submit a PR for adding more tests. + +Also of important note, typically when SSH keys are generated, the resulting file will have the type, key, and a comment. +The typical comment is just the `username@host`. This is __NOT__ part of the key. When setting your attributes, +please be sure to set only the key in the `key` field. See the example if you are still uncertain. + +#### Actions + + + + + + + + + + + + + + + + + + <\tr> + + + + + <\tr> + +
ActionDescriptionDefault
addAdds an entry to the given user's authorized_keys fileYes
removeRemoves an entry from the given user's authorized_keys file 
modifyUpdates an existing entry to the user's authorized_keys file, but only if the indicated `key` is present 
+ +__* please note that there is no `name` attribute for this resource. The name you assign is not used in the provider__ + +#### Attributes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeDescriptionDefault Value
type + A string representing the type of key. Options include `id-rsa, ssh-dss, ssh-ed25519` and others + id-rsa
options + A hash containing the key-value pairs for options. Binary options such as `no-port-forwarding` should have a value of `nil` + {}
user + The user for which this key should be added + none - __REQUIRED__
comment + a comment to add to this entry (generally the `useranme@host` is added as a comment, but this is not required) + ''
key + the actual key + none - __REQUIRED__
+ +#### Example + + +ssh_authorized_key "for remote access" do + options { 'cert-authority' => nil, :command => '/usr/bin/startup' } + user 'admin' + key 'AAAAB3NzaC1yc2EAAAADAQABAAACAQDzB76TOkrDRaevO3I1qzosRXliAuYdjcMejHwwL5v2hRqTrBePlMW6nqz8/JgLTzHn/KxzkrKLb0GlpPDrJ1KByWGYZsfydUfv7n1+5ogoA7UW7dUc4DoQtGPuy4Xe0enr88VfALlT11aWKAw8K/I39zWiPvJNX3Mks0f3/3smjLaQEnDWWWiawp5YgzJmyzsqZFZrrFCUgv7AP1EjZofWUcRvYEEjMhKsK+G2H2VCN7MpH0cJ97E0bKNQjHBrwGyMLQZUOndGakCuOuTLpikOXSpUUz5LwqCiRIj6iUtWevwk+AYLZwxPYQpCxFceVFDhPDaJQ85vweSq+HEg7hRujq9jO7vM9LIgjqg7fwQ2Ql6zO9NjXv2UalzBi0H2AbKT1V/PpNufPgolyb/dK7Jqpqu7Ytggctl2fGyLe8yVaC9gD+/BBeCl82LZI142kdXmf4WYcZgOgcRgGJrbSZjeMzX6zZpiD1AG3T7xyEn2twmC/TqptmQEAG2BBzGum+S6pU0rnOt2UJngRnviK2vptAWtRlSlsopySOXv+VbqUXhRjHRT/+2nq5Q4BWcjsZaaoo1uWh2glATRnGK995A1zJ3gWrBA+IaC6stKzjSG0KPwLjzHfPKbWjDX76D/qdo0qBN5hBiHDRfmiNqpNYS9NHACDZNVPBS5N1d5BUkyKw==' + type 'id-rsa' + comment 'gdidy@coolman.com' +end + diff --git a/libraries/ssh_config_helpers.rb b/libraries/ssh_config_helpers.rb index 709baec..3796185 100644 --- a/libraries/ssh_config_helpers.rb +++ b/libraries/ssh_config_helpers.rb @@ -15,7 +15,7 @@ def default?(path) def parse_file(path) entries = {} return entries unless ::File.exist?(path) - name = nil? + name = nil IO.foreach(path) do |line| next if line.match(/^\s*(#|\r?\n|\s*$)/) # skip lines with only comments or whitespace @@ -50,7 +50,7 @@ def parse_line(line) return matchdata.captures[0], matchdata.captures[1].strip if matchdata Chef::Log.error("Line |#{line}| does not parse correctly") - return nil, nil + nil end end end diff --git a/metadata.rb b/metadata.rb index 62b7a80..2011e25 100644 --- a/metadata.rb +++ b/metadata.rb @@ -4,7 +4,7 @@ license 'Apache 2.0' description 'LWRPs for managing SSH known_hosts and config files' long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) -version '0.10.5' +version '0.10.6' supports 'ubuntu' supports 'rhel' diff --git a/providers/authorized_keys.rb b/providers/authorized_keys.rb new file mode 100644 index 0000000..dd07b38 --- /dev/null +++ b/providers/authorized_keys.rb @@ -0,0 +1,157 @@ +include Chef::SSH::Helpers + +use_inline_resources + +def whyrun_supported? + true +end + +action :add do + validate_options(new_resource.options, "Resource #{new_resource.name}") if new_resource.options + validate_type(new_resource.type, "Resource #{new_resource.name}") + if @current_resource.exists? + action_update + else + @lines << { :options => new_resource.options, + :type => new_resource.type, + :key => new_resource.key, + :comment => new_resource.comment } + update_file + end +end + +action :update do + return unless @current_resource.exists? + current = @lines.find { |line| line[:key] == new_resource.key } + current[:options] = new_resource.options + current[:type] = new_resource.type + current[:comment] = new_resource.comment + update_file +end + +action :remove do + return unless @current_resource.exists? + @lines.reject! { |line| line[:key] == new_resource.key } + update_file +end + +def update_file + directory ::File.dirname(@path) do + action :create + owner new_resource.user + mode 00700 + end + + file @path do + action :create + mode 00600 + owner new_resource.user + content format_lines + end +end + +def format_lines + @lines.collect do |line| + joined = '' + if line[:options] + joined << line[:options].collect do |key, value| + if value.nil? || value.empty? + key.to_s + elsif value.include?(' ') && !value.include?('"') + "#{key}=\"#{value}\"" + else + "#{key}=#{value}" + end + end.join(',') + joined << ' ' + end + joined << line[:type] << ' ' << line[:key] + line[:comment] && (joined << ' ' << line[:comment]) + joined + end.join("\n") + "\n" +end + +def initialize(new_resource, run_context) + super(new_resource, run_context) + + @path = ::File.join(user_dir(new_resource.user), '.ssh', 'authorized_keys') + + load_current_resource +end + +def load_current_resource + @lines = ::File.exist?(@path) ? parse(::IO.readlines(@path)) : [] + + current_line = @lines.find { |line| line[:key] == @new_resource.key } + @current_resource = Chef::Resource::SshKnownHosts.new(@new_resource.name) + @current_resource.exists = current_line +end + +protected + +def parse(current) + current.reduce([]) do |memo, row| + line = {} + fields = extract_fields(row) + line[:options] = parse_options(fields.shift) unless types.include? fields[0] + validate_type(fields[0], @path) + line[:type] = fields[0] + line[:key] = fields[1] + line[:comment] = fields[2..-1].join(' ') if row[2] + memo << line + end +end + +def extract_fields(row) + return :comment => row if row.empty? || row[0] == '#' + + quotes = 0 + fields = [] + row.scan(/\S+/) do |match| + if quotes.even? || quotes == 0 + fields << match + else + fields[-1] << " #{match}" + end + quotes += match.count('"') + end + fields +end + +def parse_options(text) + options = {} + split = text.split(',') + split.each do |group| + validate_options(group, @path) + group = group.split('=') + options[group[0]] = group[1] + end + options +end + +def types + %w(id-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 ssh-dss) +end + +def validate_type(type, source) + valid = types + fail "Invalid Type #{type} in #{source}" unless valid.include? type.to_s +end + +def validate_options(option, source) + if option.is_a? Hash + option.each { |o| validate_options o, source } + return + elsif option.is_a? String + option = option.split('=') + end + + binary_options = %w(cert-authority no-agent-forwarding no-port-forwarding no-pty no-user-rc no-X11-forwarding) + other_options = %w(command environment from permitopen principals tunnel) + + if option[1].nil? || option[1].empty? + fail "Invalid Option in #{source}: #{option}" unless binary_options.include? option[0].to_s + else + fail "Invalid Option in #{source}: #{option}" unless other_options.include? option[0].to_s + end +end diff --git a/providers/known_hosts.rb b/providers/known_hosts.rb index 9c5da28..1aba7e2 100644 --- a/providers/known_hosts.rb +++ b/providers/known_hosts.rb @@ -34,12 +34,11 @@ def whyrun_supported? end action :remove do - if @current_resource.exists? # ~FC023 - execute "remove known_host entry for #{new_resource.host}" do - command "ssh-keygen -R #{Shellwords.escape(new_resource.host)} -f #{new_resource.path}" - user new_resource.user if new_resource.user - umask new_resource.user ? 0077 : 0022 - end + return unless @current_resource.exists? + execute "remove known_host entry for #{new_resource.host}" do + command "ssh-keygen -R #{Shellwords.escape(new_resource.host)} -f #{new_resource.path}" + user new_resource.user if new_resource.user + umask new_resource.user ? 0077 : 0022 end end diff --git a/recipes/default.rb b/recipes/default.rb index 76f5880..76c95cc 100644 --- a/recipes/default.rb +++ b/recipes/default.rb @@ -3,5 +3,5 @@ # Recipe: default.rb node['ssh']['packages'].each do |package_name| - package package_name + package package_name end diff --git a/resources/authorized_keys.rb b/resources/authorized_keys.rb new file mode 100644 index 0000000..0b08cb2 --- /dev/null +++ b/resources/authorized_keys.rb @@ -0,0 +1,21 @@ +actions :add, :remove, :update +default_action :add + +attribute :type, + :kind_of => String, + :default => 'id-rsa', + :equal_to => [ + 'id-rsa', + 'ecdsa-sha2-nistp256', + 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', + 'ssh-ed25519', + 'ssh-dss' + ] +attribute :options, :kind_of => Hash, :default => {} +attribute :key, :kind_of => String, :required => true +attribute :comment, :kind_of => String +attribute :user, :kind_of => String, :required => true + +attr_accessor :exists +alias_method :exists?, :exists diff --git a/test/cookbooks/ssh_test/recipes/authorized_keys.rb b/test/cookbooks/ssh_test/recipes/authorized_keys.rb new file mode 100644 index 0000000..811efef --- /dev/null +++ b/test/cookbooks/ssh_test/recipes/authorized_keys.rb @@ -0,0 +1,25 @@ +user 'test-user' do + manage_home true + supports :manage_home => true + home '/home/test-user' + action [:create, :manage] +end + +ssh_authorized_keys 'first' do + user 'test-user' + type 'id-rsa' + options 'no-agent-forwarding' => nil, 'no-pty' => nil, :command => 'ls /root' + comment 'I left a comment' + key 'AAAAB3NzaC1yc2EAAAADAQABAAACAQCeCRfSzGWGNsisAZpuFIS0GmHJfgms3g8okwL9h9AvoQPwgyhyri/Wlcz3eyZMvuR4/vwh9FgWpRwLxot7QSGry58GYR9tHkDT9o3m0Hlx28E+K2gbNK5SyFROx5lSfOZkCSyPjBEBmTAadpVYZBJj789oeAT3dDvsxMAqokCIjV5Ey9xBIWKapbsDiTdOHmtDhlrFZfBc75I6tTnW9WGVG6gCQtzyC/tJ2DmWJhtEz9UjxhAOUzazHM2CJ2IlF3SHm+nz7xjTWmGVRzpiellmN+2StmibuFkoZP8L//9v06gDKqp2lNSsi2SJujAsEiKAGtQu6Aa4hdxRFt87m6WSN9lusAazZvnX5s93lAmUAG+wWPnAsujkRSDwv2Ju+GdQFW3ncML7aXFOhIMViG6B98X2h9f3W6XdwQseh10QfvFZ3fAmcAvWvlEM0pGXdfKeFY0LfD7UFxTvzEfqPKnbV6SKlAIMAQ3CX+Q1sZ4nfqopZVJwHDHSL/KQeVKePdyFbZcFVE4L/zruS/fLDqiDMq9yZqMu3WkP5bp4crzguaVwHmrTG4k1XOH5jkMrUj7javMLQHWu56bj0heynhXw7gzXnC/DSgY58/1BPEy7ejsGr0RX2LBRulh84UkV0cjLs8MZyBrhS4dYwyBmtcYlh+OVVVwFimg4ayR7UlkVMw==' # rubocop:disable all +end + +ssh_authorized_keys 'without comment' do + user 'test-user' + type 'id-rsa' + key 'AAAAB3NzaC1yc2EAAAADAQABAAACAQC5ulpHLJfGxi/sMjqOSWVKbW4yeNzWF2VAZYM/QYqH/9LUuEwBAgxFB96dxDn+AY9UxM2lmiYajsHHF6qqnRPuK2SVHKZ8SSR/yEWL6AtkeX5z0QgzCqXLAgMUbzRy6TRb/SM8tqQ+ybVGiVaSK+UGT5saFY3WLmhq2wJva5TzRhxtL+vnQMNUDaFylTZDqw9Y0GFHkqEvcU2L0bcLlhEr0NVUFwz0M2yW0mx+Y8wwwUxxFhbTsjxV2c6Fa/bmqSN1I6z5K0hYQRyPlNpjEKlKT57TTHrlSCsf+Id2DOC1EbEVcCtIT6tIlahys9qayYM/36nbD0ru0NgDY2NliWlTk+F7L9gBncPdCib9euz3JqyftnNGT9OzpK8A9U4nKDVdaQKVN6Eat4ViEmwBnO8CeaT2XDKVLOJcRnaZu6pYFvbZcyBC0DPThksvl1pTlnBr7aypW/15lkXY1vNPa6mvyfFjO5SHDCBvMRSaCB4NqGreaVaomvnvMv+wwn+PCNmvTSv2PfZzjWHbp5wip7x9bNmF8s1w2knOR3appjEDnJm6YR4KuXMRxr4zsAxVvC5uOhs4JVh91Ujtec/aVKXBQuggT8tX00M6rSamMbvNkmlA6WIVrwmYvCRIo8GqKKxU8lJJcdt/y/qa4jIdd5Z6mWQmhy8Jw8La7J2GWvX3Ow==' # rubocop:disable all +end + +ssh_authorized_keys 'type defaults' do + user 'test-user' + key 'AAAAB3NzaC1yc2EAAAADAQABAAACAQDzB76TOkrDRaevO3I1qzosRXliAuYdjcMejHwwL5v2hRqTrBePlMW6nqz8/JgLTzHn/KxzkrKLb0GlpPDrJ1KByWGYZsfydUfv7n1+5ogoA7UW7dUc4DoQtGPuy4Xe0enr88VfALlT11aWKAw8K/I39zWiPvJNX3Mks0f3/3smjLaQEnDWWWiawp5YgzJmyzsqZFZrrFCUgv7AP1EjZofWUcRvYEEjMhKsK+G2H2VCN7MpH0cJ97E0bKNQjHBrwGyMLQZUOndGakCuOuTLpikOXSpUUz5LwqCiRIj6iUtWevwk+AYLZwxPYQpCxFceVFDhPDaJQ85vweSq+HEg7hRujq9jO7vM9LIgjqg7fwQ2Ql6zO9NjXv2UalzBi0H2AbKT1V/PpNufPgolyb/dK7Jqpqu7Ytggctl2fGyLe8yVaC9gD+/BBeCl82LZI142kdXmf4WYcZgOgcRgGJrbSZjeMzX6zZpiD1AG3T7xyEn2twmC/TqptmQEAG2BBzGum+S6pU0rnOt2UJngRnviK2vptAWtRlSlsopySOXv+VbqUXhRjHRT/+2nq5Q4BWcjsZaaoo1uWh2glATRnGK995A1zJ3gWrBA+IaC6stKzjSG0KPwLjzHfPKbWjDX76D/qdo0qBN5hBiHDRfmiNqpNYS9NHACDZNVPBS5N1d5BUkyKw==' # rubocop:disable all +end diff --git a/test/integration/authorized_keys/serverspec/authorized_keys_spec.rb b/test/integration/authorized_keys/serverspec/authorized_keys_spec.rb new file mode 100644 index 0000000..9ea4274 --- /dev/null +++ b/test/integration/authorized_keys/serverspec/authorized_keys_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe user('test-user') do + it { should exist } + # rubocop:disable all + it { should have_authorized_key 'no-agent-forwarding,no-pty,command="ls /root" id-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCeCRfSzGWGNsisAZpuFIS0GmHJfgms3g8okwL9h9AvoQPwgyhyri/Wlcz3eyZMvuR4/vwh9FgWpRwLxot7QSGry58GYR9tHkDT9o3m0Hlx28E+K2gbNK5SyFROx5lSfOZkCSyPjBEBmTAadpVYZBJj789oeAT3dDvsxMAqokCIjV5Ey9xBIWKapbsDiTdOHmtDhlrFZfBc75I6tTnW9WGVG6gCQtzyC/tJ2DmWJhtEz9UjxhAOUzazHM2CJ2IlF3SHm+nz7xjTWmGVRzpiellmN+2StmibuFkoZP8L//9v06gDKqp2lNSsi2SJujAsEiKAGtQu6Aa4hdxRFt87m6WSN9lusAazZvnX5s93lAmUAG+wWPnAsujkRSDwv2Ju+GdQFW3ncML7aXFOhIMViG6B98X2h9f3W6XdwQseh10QfvFZ3fAmcAvWvlEM0pGXdfKeFY0LfD7UFxTvzEfqPKnbV6SKlAIMAQ3CX+Q1sZ4nfqopZVJwHDHSL/KQeVKePdyFbZcFVE4L/zruS/fLDqiDMq9yZqMu3WkP5bp4crzguaVwHmrTG4k1XOH5jkMrUj7javMLQHWu56bj0heynhXw7gzXnC/DSgY58/1BPEy7ejsGr0RX2LBRulh84UkV0cjLs8MZyBrhS4dYwyBmtcYlh+OVVVwFimg4ayR7UlkVMw== I left a comment' } + it { should have_authorized_key 'id-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDzB76TOkrDRaevO3I1qzosRXliAuYdjcMejHwwL5v2hRqTrBePlMW6nqz8/JgLTzHn/KxzkrKLb0GlpPDrJ1KByWGYZsfydUfv7n1+5ogoA7UW7dUc4DoQtGPuy4Xe0enr88VfALlT11aWKAw8K/I39zWiPvJNX3Mks0f3/3smjLaQEnDWWWiawp5YgzJmyzsqZFZrrFCUgv7AP1EjZofWUcRvYEEjMhKsK+G2H2VCN7MpH0cJ97E0bKNQjHBrwGyMLQZUOndGakCuOuTLpikOXSpUUz5LwqCiRIj6iUtWevwk+AYLZwxPYQpCxFceVFDhPDaJQ85vweSq+HEg7hRujq9jO7vM9LIgjqg7fwQ2Ql6zO9NjXv2UalzBi0H2AbKT1V/PpNufPgolyb/dK7Jqpqu7Ytggctl2fGyLe8yVaC9gD+/BBeCl82LZI142kdXmf4WYcZgOgcRgGJrbSZjeMzX6zZpiD1AG3T7xyEn2twmC/TqptmQEAG2BBzGum+S6pU0rnOt2UJngRnviK2vptAWtRlSlsopySOXv+VbqUXhRjHRT/+2nq5Q4BWcjsZaaoo1uWh2glATRnGK995A1zJ3gWrBA+IaC6stKzjSG0KPwLjzHfPKbWjDX76D/qdo0qBN5hBiHDRfmiNqpNYS9NHACDZNVPBS5N1d5BUkyKw==' } + it { should have_authorized_key 'id-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC5ulpHLJfGxi/sMjqOSWVKbW4yeNzWF2VAZYM/QYqH/9LUuEwBAgxFB96dxDn+AY9UxM2lmiYajsHHF6qqnRPuK2SVHKZ8SSR/yEWL6AtkeX5z0QgzCqXLAgMUbzRy6TRb/SM8tqQ+ybVGiVaSK+UGT5saFY3WLmhq2wJva5TzRhxtL+vnQMNUDaFylTZDqw9Y0GFHkqEvcU2L0bcLlhEr0NVUFwz0M2yW0mx+Y8wwwUxxFhbTsjxV2c6Fa/bmqSN1I6z5K0hYQRyPlNpjEKlKT57TTHrlSCsf+Id2DOC1EbEVcCtIT6tIlahys9qayYM/36nbD0ru0NgDY2NliWlTk+F7L9gBncPdCib9euz3JqyftnNGT9OzpK8A9U4nKDVdaQKVN6Eat4ViEmwBnO8CeaT2XDKVLOJcRnaZu6pYFvbZcyBC0DPThksvl1pTlnBr7aypW/15lkXY1vNPa6mvyfFjO5SHDCBvMRSaCB4NqGreaVaomvnvMv+wwn+PCNmvTSv2PfZzjWHbp5wip7x9bNmF8s1w2knOR3appjEDnJm6YR4KuXMRxr4zsAxVvC5uOhs4JVh91Ujtec/aVKXBQuggT8tX00M6rSamMbvNkmlA6WIVrwmYvCRIo8GqKKxU8lJJcdt/y/qa4jIdd5Z6mWQmhy8Jw8La7J2GWvX3Ow==' } + # rubocop:enable all +end diff --git a/test/integration/authorized_keys/serverspec/spec_helper.rb b/test/integration/authorized_keys/serverspec/spec_helper.rb new file mode 100644 index 0000000..37af1b4 --- /dev/null +++ b/test/integration/authorized_keys/serverspec/spec_helper.rb @@ -0,0 +1,3 @@ +require 'serverspec' + +set :backend, :exec