ssh-allow 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|