ssh-allow 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.md +24 -0
- data/README.md +70 -0
- data/Rakefile +2 -0
- data/bin/ssh-allow +4 -0
- data/lib/ssh/allow.rb +4 -0
- data/lib/ssh/allow/cli.rb +33 -0
- data/lib/ssh/allow/command.rb +41 -0
- data/lib/ssh/allow/command_line.treetop +104 -0
- data/lib/ssh/allow/rule.rb +97 -0
- data/lib/ssh/allow/rule_set.rb +38 -0
- data/lib/ssh/allow/version.rb +5 -0
- data/spec/acceptance/acceptance_helper.rb +21 -0
- data/spec/acceptance/cli_spec.rb +47 -0
- data/spec/command_line_spec.rb +197 -0
- data/spec/command_spec.rb +72 -0
- data/spec/rule_set_spec.rb +95 -0
- data/spec/rule_spec.rb +219 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/.gitkeep +0 -0
- data/spec/support/cli_helpers.rb +17 -0
- data/spec/support/rule_set_helpers.rb +12 -0
- data/ssh-allow.gemspec +30 -0
- metadata +196 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# License & Copywrite
|
2
|
+
|
3
|
+
**Copyright (c) 2008 by the Trustees of Dartmouth College. All rights reserved.**
|
4
|
+
|
5
|
+
The remote_key Ruby library, and accompanying documentation, are provided
|
6
|
+
subject to the following license agreement. By obtaining and/or using the
|
7
|
+
software, you agree that you have read and understood this agreement, and
|
8
|
+
will comply with its terms and conditions.
|
9
|
+
|
10
|
+
1. Permission to use, copy, modify and distribute this software and
|
11
|
+
documentation without fee is hereby granted, provided that the copyright
|
12
|
+
notice and this agreement appear on all copies of the software and
|
13
|
+
documentation.
|
14
|
+
|
15
|
+
2. Any documentation and advertising material relating to this software must
|
16
|
+
acknowledge that the software was developed by Dartmouth College.
|
17
|
+
|
18
|
+
3. The name of Dartmouth College may not be used to endorse or promote
|
19
|
+
products derived from this software without specific prior written
|
20
|
+
permission.
|
21
|
+
|
22
|
+
4. Neither the Trustees of Dartmouth College nor the authors make any
|
23
|
+
representations about the suitability of this software for any purpose. It is
|
24
|
+
provided "as is", without any express or implied warranty.
|
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# ssh-allow
|
2
|
+
|
3
|
+
A mini-DSL for specifying which remote SSH commands are allowed (or denied) during a remote shell session, authenticated via an SSH key. Also comes with a command-line binary for comparing the current remote command against the list.
|
4
|
+
|
5
|
+
## Purpose
|
6
|
+
|
7
|
+
Guarding against the SSH_ORIGINAL_COMMAND environment is fairly command and allowing/denying simple commands is easy to do with a simple bash shell. Unfortunately, if what you need to do is allow a very narrow set of non-trivial commands, where the various command-line options and arguments need to be specified, trying to accomplish this with a bash script quickly becomes arduous.
|
8
|
+
|
9
|
+
To try and solve this problem, I developed an extremely simple _DSL_, which is really just some stripped down ruby code, that would make specifying almost any range of commands, with almost any range of options and/or arguments, a straightforward problem. Those **rules** needed a specialized command-line binary to both process them and compare the allowed (or denied) commands against ENV['SSH_ORIGINAL_COMMAND'].
|
10
|
+
|
11
|
+
That's ssh-allow.
|
12
|
+
|
13
|
+
---
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
$ sudo gem install ssh-allow
|
18
|
+
|
19
|
+
---
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
### guard (default command)
|
24
|
+
|
25
|
+
$ ssh-allow guard [-r | --rules=<file>] [-e | --echo]
|
26
|
+
|
27
|
+
The --rules `<file>` path defaults to `~/.ssh-rules`. If the intent is to deploy ssh-allow across multiple accounts on a single Unix/Linux server, then it's recommended that you create `/etc/ssh-allow/` to hold the various rules files.
|
28
|
+
|
29
|
+
The --echo switch echos the command to std_in, prior to executing it.
|
30
|
+
|
31
|
+
The guard command is intended to be specified in the ~/.ssh/authorized_keys file, as the front-part of the line containing the SSH key that needs command guarding. The configuration usually looks like this:
|
32
|
+
|
33
|
+
command="/usr/local/bin/ssh-allow guard -r=/etc/ssh-allow/rule_file" ssh-rsa AAAAB3Nza
|
34
|
+
|
35
|
+
---
|
36
|
+
|
37
|
+
## Rules Specification DSL
|
38
|
+
|
39
|
+
Allowing an `ls` command, with no options and no arguments.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
allow!('ls')
|
43
|
+
```
|
44
|
+
|
45
|
+
Allowing an `ls` command, with any options and any arguments.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
allow!('ls') do
|
49
|
+
opts :any
|
50
|
+
args :any
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
Allowing an `ls` command, with a specific option and a file-path argument regex.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
allow!('ls') do
|
58
|
+
opts '-ld'
|
59
|
+
args '^/foo/bar/.*'
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
Allowing a `cp` command, with no options and two file-path argument regexes.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
allow!('cp') do
|
67
|
+
args '^/foo/bar/.*'
|
68
|
+
args '^/foo/baz/.*'
|
69
|
+
end
|
70
|
+
```
|
data/Rakefile
ADDED
data/bin/ssh-allow
ADDED
data/lib/ssh/allow.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module SSH ; module Allow
|
4
|
+
class CLI < Thor
|
5
|
+
|
6
|
+
desc "guard --rules | -r=<file> [--echo | -e]",
|
7
|
+
"Guard against the SSH_ORIGINAL_COMMAND, using rules in the --rules file."
|
8
|
+
method_option :rules, :type => :string, :aliases => '-r', :required => true,
|
9
|
+
:default => File.expand_path("~/.ssh-rules"), :banner => "Path to rules file"
|
10
|
+
method_option :echo, :type => :boolean, :default => false, :aliases => '-e',
|
11
|
+
:banner => "Echo the SSH_ORIGINAL_COMMAND."
|
12
|
+
def guard
|
13
|
+
rule_set.read(options[:rules])
|
14
|
+
puts command if options[:echo]
|
15
|
+
command.allowed?(rule_set.rules) ? command.run : fail
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def rule_set
|
21
|
+
@rule_set ||= SSH::Allow::RuleSet.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def command
|
25
|
+
@command ||= SSH::Allow::Command.new(ENV['SSH_ORIGINAL_COMMAND'])
|
26
|
+
end
|
27
|
+
|
28
|
+
def fail
|
29
|
+
raise Thor::Error, "Remote Command Not Allowed: #{command}"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end ; end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'polyglot'
|
4
|
+
require 'treetop'
|
5
|
+
require 'command_line'
|
6
|
+
|
7
|
+
module SSH ; module Allow
|
8
|
+
class Command
|
9
|
+
attr_reader :name, :options, :arguments
|
10
|
+
|
11
|
+
def initialize(cmd)
|
12
|
+
@cmd = cmd.to_s
|
13
|
+
@name = parsed.name.text_value
|
14
|
+
@options = parsed.option_list
|
15
|
+
@arguments = parsed.argument_list
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
@cmd
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
system(@cmd)
|
24
|
+
end
|
25
|
+
|
26
|
+
def allowed?(rules)
|
27
|
+
match, allow = false, false
|
28
|
+
rules.each do |rule|
|
29
|
+
match, allow = rule.match?(self)
|
30
|
+
break if match
|
31
|
+
end
|
32
|
+
allow
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def parsed
|
38
|
+
@parsed ||= CommandLineParser.new.parse(@cmd)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end ; end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
grammar CommandLine
|
2
|
+
rule command
|
3
|
+
name (delimited_option / delimited_argument)* {
|
4
|
+
def option_list
|
5
|
+
opts = self.elements[1].elements.inject([]) do |a, e|
|
6
|
+
a << e.opt_value
|
7
|
+
end
|
8
|
+
opts.compact
|
9
|
+
end
|
10
|
+
|
11
|
+
def argument_list
|
12
|
+
args = self.elements[1].elements.inject([]) do |a, e|
|
13
|
+
a << e.arg_value
|
14
|
+
end
|
15
|
+
args.compact
|
16
|
+
end
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
rule name
|
21
|
+
argument
|
22
|
+
end
|
23
|
+
|
24
|
+
rule delimited_option
|
25
|
+
' ' option {
|
26
|
+
def opt_value
|
27
|
+
self.option.opt_value
|
28
|
+
end
|
29
|
+
|
30
|
+
def arg_value
|
31
|
+
self.option.arg_value
|
32
|
+
end
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
rule delimited_argument
|
37
|
+
' ' argument {
|
38
|
+
def opt_value
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def arg_value
|
43
|
+
self.argument.text_value
|
44
|
+
end
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
rule option
|
49
|
+
(long_option / short_option)
|
50
|
+
end
|
51
|
+
|
52
|
+
rule long_option
|
53
|
+
'--' (opt_name '=' argument / opt_name) {
|
54
|
+
def opt_value
|
55
|
+
self.elements[1].respond_to?(:opt_name) ?
|
56
|
+
self.elements[1].opt_name.text_value : self.elements[1].text_value
|
57
|
+
end
|
58
|
+
|
59
|
+
def arg_value
|
60
|
+
self.elements[1].respond_to?(:opt_name) ?
|
61
|
+
self.elements[1].argument.text_value : nil
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
rule short_option
|
67
|
+
'-' (label_char '=' argument / opt_name) {
|
68
|
+
def opt_value
|
69
|
+
self.elements[1].respond_to?(:label_char) ?
|
70
|
+
self.elements[1].label_char.text_value : self.elements[1].text_value
|
71
|
+
end
|
72
|
+
|
73
|
+
def arg_value
|
74
|
+
self.elements[1].respond_to?(:label_char) ?
|
75
|
+
self.elements[1].argument.text_value : nil
|
76
|
+
end
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
rule argument
|
81
|
+
(quoted_chars / unquoted_chars) {
|
82
|
+
def arg_value
|
83
|
+
self.text_value
|
84
|
+
end
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
rule opt_name
|
89
|
+
label_char+
|
90
|
+
end
|
91
|
+
|
92
|
+
rule label_char
|
93
|
+
[a-zA-Z0-9]
|
94
|
+
end
|
95
|
+
|
96
|
+
rule quoted_chars
|
97
|
+
'"' (!'"' . / '\"')* '"'
|
98
|
+
end
|
99
|
+
|
100
|
+
rule unquoted_chars
|
101
|
+
(!' ' . / '\ ')+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module SSH ; module Allow
|
2
|
+
|
3
|
+
class Rule
|
4
|
+
attr_reader :command, :options, :arguments
|
5
|
+
|
6
|
+
def initialize(cmds)
|
7
|
+
@command = [cmds].flatten
|
8
|
+
@options = [:none]
|
9
|
+
@arguments = [:none]
|
10
|
+
end
|
11
|
+
|
12
|
+
def opts(opt_text)
|
13
|
+
opt_text.gsub!(/^--?/, '') if opt_text.is_a?(String)
|
14
|
+
push(:options, opt_text)
|
15
|
+
end
|
16
|
+
|
17
|
+
def args(arg_text)
|
18
|
+
arg_text = Regexp.new(arg_text) if arg_text.is_a?(String)
|
19
|
+
push(:arguments, arg_text)
|
20
|
+
end
|
21
|
+
|
22
|
+
def match_command?(name)
|
23
|
+
return false if none?(command)
|
24
|
+
return true if any?(command)
|
25
|
+
command.include?(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def match_options?(opt_list)
|
29
|
+
return opt_list.empty? if none?(options)
|
30
|
+
return true if any?(options)
|
31
|
+
return false if options.size != opt_list.size
|
32
|
+
opt_list.inject(true) { |match, opt| match && options.include?(opt) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def match_arguments?(arg_list)
|
36
|
+
return arg_list.empty? if none?(arguments)
|
37
|
+
return true if any?(arguments)
|
38
|
+
return false if arguments.size != arg_list.size
|
39
|
+
arg_list.inject(true) { |match, arg| match && match_one_argument?(arg) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def match_one_argument?(arg)
|
43
|
+
arguments.inject(false) { |match, one_arg| match || (one_arg === arg) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def push(attrib, value)
|
47
|
+
send(attrib).clear if send(attrib) == [:none]
|
48
|
+
send(attrib).push(value)
|
49
|
+
end
|
50
|
+
|
51
|
+
def none?(part)
|
52
|
+
part == [:none]
|
53
|
+
end
|
54
|
+
|
55
|
+
def any?(part)
|
56
|
+
part == [:any]
|
57
|
+
end
|
58
|
+
|
59
|
+
private :push, :none?, :any?, :match_one_argument?
|
60
|
+
|
61
|
+
class << self
|
62
|
+
def allow(*cmds, &block)
|
63
|
+
create(Rule::Allow.new(cmds.flatten), &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def deny(*cmds, &block)
|
67
|
+
create(Rule::Deny.new(cmds.flatten), &block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def create(rule, &block)
|
71
|
+
begin
|
72
|
+
rule.instance_eval(&block) if block_given?
|
73
|
+
rule
|
74
|
+
rescue Exception => e
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Allow < Rule
|
81
|
+
def match?(command)
|
82
|
+
match = [match_command?(command.name), match_options?(command.options),
|
83
|
+
match_arguments?(command.arguments)].inject(true) { |mem, var| var && mem }
|
84
|
+
return match, match
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Deny < Rule
|
89
|
+
def match?(command)
|
90
|
+
match = [match_command?(command.name), match_options?(command.options),
|
91
|
+
match_arguments?(command.arguments)].inject(true) { |mem, var| var && mem }
|
92
|
+
return match, !match
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end ; end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SSH ; module Allow
|
2
|
+
|
3
|
+
class RuleSet
|
4
|
+
attr_reader :rules
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@rules = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def allow(cmd, &block)
|
11
|
+
push get_rule(:allow, cmd, block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def allow!(cmd, &block)
|
15
|
+
rule = get_rule(:allow, cmd, block)
|
16
|
+
push(rule) or raise(%(Invalid rule: "#{cmd}"))
|
17
|
+
end
|
18
|
+
|
19
|
+
def read(path_to_rules)
|
20
|
+
self.instance_eval(read_rules(path_to_rules))
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def push(rule)
|
26
|
+
rule ? @rules.push(rule) : false
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_rule(type, cmd, block)
|
30
|
+
SSH::Allow::Rule.send(type, cmd, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_rules(path_to_rules)
|
34
|
+
IO.read(path_to_rules)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end ; end
|