hoosegow 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 08457775221dac7f850e7f3ba83e41661a45ae62
4
+ data.tar.gz: 64919e4a0404a832ff1cd700bb8b665af96fd63e
5
+ SHA512:
6
+ metadata.gz: fe8880787a543fca141ec9c2f59df2ae778dc7d791709c7a9f39e304f2ea0f16800d79d3236c0e28f4feff3fb7afc3077c124c4a645be2206a46f4dc85d8e6d6
7
+ data.tar.gz: edda7d73693b4944a81dc1cbdbf699feb2ffba8507412fb12d0207188fcf74cead9732fd37cad938516eccdb007c2d24e513cfcb350bb915d23ed35eff63669b
data/Dockerfile ADDED
@@ -0,0 +1,46 @@
1
+ # Ubuntu base image
2
+ FROM ubuntu
3
+
4
+ # Install rbenv deps
5
+ RUN apt-get update
6
+ RUN apt-get install -y build-essential zlib1g-dev libssl-dev openssl libreadline-dev sqlite3 libsqlite3-dev libxslt-dev libxml2-dev curl wget git-core
7
+
8
+ # Install rbenv
9
+ RUN git clone https://github.com/sstephenson/rbenv.git /.rbenv
10
+ RUN echo 'export PATH="/.rbenv/bin:$PATH"' >> /etc/profile
11
+ RUN echo 'export RBENV_ROOT="/.rbenv"' >> /etc/profile
12
+ RUN echo 'eval "$(rbenv init -)"' >> /etc/profile
13
+ RUN echo 'gem: --no-rdoc --no-ri' >> /etc/gemrc
14
+
15
+ # Install rbenv build plugin
16
+ RUN mkdir -p /.rbenv/plugins
17
+ RUN git clone https://github.com/sstephenson/ruby-build.git /.rbenv/plugins/ruby-build
18
+
19
+ # Install specified ruby version
20
+ RUN /bin/bash -l -c 'RUBY_CONFIGURE_OPTS="--disable-install-doc" rbenv install {{ruby_version}}'
21
+ RUN /bin/bash -l -c 'rbenv global {{ruby_version}}'
22
+ RUN /bin/bash -l -c 'gem install bundler'
23
+ RUN /bin/bash -l -c 'rbenv rehash'
24
+
25
+ # Create a user to run as.
26
+ RUN adduser --no-create-home --disabled-password --gecos "" --shell /bin/false hoosegow
27
+
28
+ ###########################################################################################
29
+ # Anything added after the ADD command will not be cached. Try to add changes above here. #
30
+ ###########################################################################################
31
+
32
+ # Add this directory to /
33
+ ADD . /hoosegow
34
+ RUN chown -R hoosegow:hoosegow /hoosegow
35
+
36
+ # Switch to limited user now.
37
+ USER hoosegow
38
+
39
+ # Run all commands in /hoosegow
40
+ WORKDIR /hoosegow
41
+
42
+ # Bundle hoosegow
43
+ RUN /bin/bash -l -c 'BUNDLE_JOBS=4 bundle install --path .bundle --without development test'
44
+
45
+ # Command to run when `docker run hoosegow`
46
+ ENTRYPOINT /bin/bash -l -c bin/hoosegow
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ # Load dependency Gemfile if present.
6
+ inmate_gemfile = File.join(File.dirname(__FILE__), 'inmate', 'Gemfile')
7
+ if File.exist? inmate_gemfile
8
+ eval(IO.read(inmate_gemfile), binding)
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 GitHub, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # Hoosegow
2
+
3
+ Ephemeral Docker jails for running untrusted Ruby code.
4
+
5
+ Hoosegow runs both in your code and in a Docker container. When you call a method on a Hoosegow instance, it proxies the method call to another instance of Hoosegow running inside a Docker container.
6
+
7
+ # Security
8
+
9
+ Hoosegow is intended to add a layer of security to projects that need to run code that is not fully trusted/audited. Because the untrusted code is running inside a Docker container, an attacker who manages to exploit a vulnerability in the code must also break out of the Docker container before gaining any access to the host system.
10
+
11
+ This means that Hoosegow is only as strong as Docker. Docker employs Kernel namespaces, capabilities, and cgroups to contain processes running inside a container. This is not true Virtualization though, and a process running as root inside the container *can* compromise the host system. Any priviledge escalation bugs in the host Kernel could also be used to become root and compromise the host machine. Further hardening of the base Ubuntu image, along with tools like AppArmor or SE-Linux can improve the security posture of an application relying on Hoosegow/Docker.
12
+
13
+ The following are some useful resources regarding the security of Docker:
14
+
15
+ - The [Docker Security](https://docs.docker.com/articles/security/) article from Docker.io.
16
+ - The [LXC, Docker, Security](http://www.slideshare.net/jpetazzo/linux-containers-lxc-docker-and-security) slides from Jérôme Petazzoni.
17
+ - The series of Docker security articles from Daniel J. Walsh ([one](http://opensource.com/business/14/7/docker-security-selinux), [two](http://opensource.com/business/14/9/security-for-docker)).
18
+
19
+ #### Installing
20
+
21
+ Gems are available from the [releases page](https://github.com/github/hoosegow/releases). Download a gem to
22
+ your app's `vendor/cache` directory, and add this to your Gemfile:
23
+
24
+ gem "hoosegow"
25
+
26
+ #### Defining Methods to Proxy
27
+
28
+ You need to define the methods you want to have run in the Docker container. To do this, you need to create a `inmate.rb` file that defines a `Hoosegow::Inmate` module. Any methods on this module will be available on `Hoosegow` instances and will be proxied to the Docker container. Here is an example `inmate.rb` file:
29
+
30
+ ```ruby
31
+ class Hoosegow
32
+ module Inmate
33
+ def reverse(input)
34
+ input.reverse
35
+ end
36
+ end
37
+ end
38
+ ```
39
+
40
+ The `inmate.rb` file should be in its own folder, with an optional `Gemfile` to specify dependencies. This directory will be copied to the Docker container at build time so your methods are available to be proxied to. You specify the location of the directory containing the `inmate.rb` file when instantiating a `Hoosegow` object:
41
+
42
+ ```ruby
43
+ hoosegow = Hoosegow.new :inmate_dir => File.join(RAILS_ROOT, "hoosegow_deps")
44
+ hoosegow.reverse "foobar"
45
+ #=> "raboof"
46
+ ```
47
+
48
+ #### Building the Docker Image
49
+
50
+ Before you can start using Hoosegow, you need to build the Docker image that Hoosegow will proxy method calls to. This can be done in a rake task or bootstrap script:
51
+
52
+ ```ruby
53
+ hoosegow = Hoosegow.new :inmate_dir => File.join(RAILS_ROOT, "hoosegow_deps")
54
+ hoosegow.build_image
55
+ hoosegow.image_name
56
+ #=> "hoosegow:2f8f155e72828ddab9bd8bd0e355c47fb01a5323"
57
+ ```
58
+
59
+ The image will need to be rebuilt with any changes to Hoosegow or the `inmate.rb` file. If the image is built ahead of time (by a rake task or bootstrap script), you can pass the name of the image to use when instantiating a Hoosegow instance:
60
+
61
+ ```ruby
62
+ ENV['HOOSEGOW_IMAGE']
63
+ #=> "hoosegow:2f8f155e72828ddab9bd8bd0e355c47fb01a5323"
64
+ hoosegow = Hoosegow.new :inmate_dir => File.join(RAILS_ROOT, "hoosegow_deps")
65
+ :image_name => ENV['HOOSEGOW_IMAGE']
66
+ ```
67
+
68
+ #### Configuring the Connection to Docker
69
+
70
+ By default Docker's API listens locally on a Unix socket. If you are running Docker with it's default configuration, you don't need to worry about configuring Hoosegow.
71
+
72
+ **Configure Hoosegow to connect to a non-standard Unix socket.**
73
+
74
+ ```ruby
75
+ Hoosegow.new :socket => '/path/to/socket'
76
+ ```
77
+
78
+ **Configure Hoosegow to connect to a Docker daemon running on another computer.**
79
+
80
+ ```ruby
81
+ Hoosegow.new :host => '192.168.1.192', :port => 4243
82
+ ```
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require_relative 'lib/hoosegow'
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ begin
6
+ require_relative 'config'
7
+ rescue LoadError
8
+ CONFIG = {}
9
+ end
10
+
11
+ inmate_dir = File.join(File.dirname(__FILE__), 'spec', 'test_inmate')
12
+ CONFIG[:inmate_dir] = inmate_dir
13
+ CONFIG[:image_name] = Hoosegow.new(CONFIG).image_name
14
+
15
+ RSpec::Core::RakeTask.new(:spec)
16
+ Rake::Task[:spec].prerequisites << :bootstrap_docker
17
+ task :default => :spec
18
+
19
+ def hoosegow
20
+ @hoosgow ||= Hoosegow.new CONFIG
21
+ end
22
+
23
+ desc "Benchmark render_reverse run in docker"
24
+ task :benchmark => :bootstrap_docker do
25
+ 10.times do |i|
26
+ sleep 0.5
27
+ start = Time.now
28
+ hoosegow.render_reverse "foobar"
29
+ puts "render_reverse run ##{i} took #{Time.now - start} seconds"
30
+ end
31
+ hoosegow.cleanup
32
+ end
33
+
34
+ desc "Building docker image."
35
+ task :bootstrap_docker do
36
+ hoosegow.build_image unless hoosegow.image_exists?
37
+ end
data/bin/hoosegow ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # See docs/dispatch.md for more information.
3
+
4
+ require_relative '../lib/hoosegow'
5
+
6
+ real_stdout = $stdout.dup
7
+ real_stdout.sync = true
8
+ intercepted_stdout, intercepting_stdout = IO.pipe # returns reader,writer
9
+ $stdout.reopen(intercepting_stdout)
10
+
11
+ Hoosegow::Protocol::Inmate.run({
12
+ :stdout => real_stdout,
13
+ :intercepted => intercepted_stdout,
14
+ })
@@ -0,0 +1,13 @@
1
+ title Hoosegow method dispatch
2
+
3
+ App->Hoosegow: do_work(args)
4
+ Hoosegow->Docker: start and attach to instance
5
+ Hoosegow->bin/hoosegow: stdin: msgpack(:do_work, args)
6
+ bin/hoosegow->Inmate: send(:do_work, args)
7
+ Inmate->bin/hoosegow: yield("message")
8
+ bin/hoosegow->Hoosegow: stdout: msgpack(:yield, "message")
9
+ Hoosegow->App: yield("message")
10
+ Inmate->bin/hoosegow: return :ok
11
+ bin/hoosegow->Hoosegow: stdout: msgpack(:return, "ok")
12
+ Hoosegow->Docker: stop container
13
+ Hoosegow->App: return "ok"
data/docs/dispatch.md ADDED
@@ -0,0 +1,53 @@
1
+ # Hoosegow dispatch
2
+
3
+ Calling a method on an inmate involves passing information through several layers. The participants in the flow are:
4
+
5
+ * **App** - your app's code, outside of the container.
6
+ * **Hoosegow** - an instance of the `Hoosegow` class, created outside the container.
7
+ * **Docker** - the docker daemon.
8
+ * **`bin/hoosegow`** - the entry point in the docker container.
9
+ * **Inmate** - an instance of the `Hoosegow` class, inside the container, that has included `Hoosegow::Inmate` (your sandboxed code).
10
+
11
+ ![](dispatch.png)
12
+
13
+ ## Interfaces
14
+
15
+ ### Hoosegow (outside the container)
16
+
17
+ The outer instance of `Hoosegow` receives a normal call from the application.
18
+
19
+ It encodes the method name and arguments and provides that to an attach call to Docker.
20
+
21
+ It reads the Docker response as it comes in. It demultiplexes the docker output into `STDERR` and `STDOUT`. `STDOUT` is further decoded into the actual `STDOUT` from the Inmate, `yield` calls, and the method result.
22
+
23
+ ### Docker
24
+
25
+ Docker receives an HTTP POST with a body, and returns an HTTP response with a body. The HTTP request body is treated as `STDIN`, and the response body contains `STDOUT` and `STDERR`.
26
+
27
+ See [documentation for attach](http://docs.docker.io/en/latest/reference/api/docker_remote_api_v1.7/#attach-to-a-container).
28
+
29
+ ### bin/hoosegow
30
+
31
+ `bin/hoosegow` is started by Docker when the container starts. It is the entry point of the container.
32
+
33
+ It decodes `STDIN` and calls the requested method on the Inmate.
34
+
35
+ `STDOUT` is reopened so that we can encode a few things on it:
36
+
37
+ * normal stdout from the inmate or child processes spawned by the inmate
38
+ * data that is `yield`ed
39
+ * the return value from the inmate.
40
+
41
+ `STDERR` is left as-is.
42
+
43
+ ### Inmate
44
+
45
+ The Inmate is an object that includes the `Hoosegow::Inmate` module.
46
+
47
+ Input to the Inmate is a method name and arguments.
48
+
49
+ Output from the Inmate can be spread across several things:
50
+
51
+ * `STDOUT` and `STDERR` - e.g. puts calls
52
+ * `yield` to a block
53
+ * the result of the method
data/docs/dispatch.png ADDED
Binary file
data/hoosegow.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ require 'rake'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'hoosegow'
5
+ s.version = '1.2.0'
6
+ s.summary = "A Docker jail for ruby code"
7
+ s.description = "Hoosegow provides an RPC layer on top of Docker containers so that you can isolate unsafe parts of your application."
8
+ s.authors = ["Ben Toews", "Matt Burke"]
9
+ s.email = 'mastahyeti@github.com'
10
+ s.licenses = ["MIT"]
11
+ globs = %w[
12
+ README.md
13
+ LICENSE
14
+ Gemfile
15
+ Rakefile
16
+ Dockerfile
17
+ hoosegow.gemspec
18
+ docs/**/*
19
+ lib/**/*
20
+ script/**/*
21
+ spec/**/*
22
+ ]
23
+ s.files = Dir[*globs]
24
+ s.executables = ['hoosegow']
25
+ s.homepage = 'https://github.com/github/hoosegow'
26
+ s.required_ruby_version = ">= 1.9.3"
27
+ s.add_development_dependency 'rake', '>= 10.3.2', '~> 10.3'
28
+ s.add_development_dependency 'rspec', '>= 2.14.1', '~> 2.14'
29
+ s.add_runtime_dependency 'msgpack', '>= 0.5.6', '~> 0.5'
30
+ s.add_runtime_dependency 'yajl-ruby', '>= 1.1.0', '~> 1.1'
31
+ s.add_runtime_dependency 'docker-api', '~> 1.13.6'
32
+ end
data/lib/hoosegow.rb ADDED
@@ -0,0 +1,160 @@
1
+ require_relative 'hoosegow/docker'
2
+ require_relative 'hoosegow/exceptions'
3
+ require_relative 'hoosegow/image_bundle'
4
+ require_relative 'hoosegow/protocol'
5
+
6
+ class Hoosegow
7
+ # Public: Initialize a Hoosegow instance.
8
+ #
9
+ # options -
10
+ # :no_proxy - Development mode. Use this if you don't want to
11
+ # setup Docker on your development instance, but
12
+ # still need to test rendering files. This is how
13
+ # Hoosegow runs inside the Docker container.
14
+ # :inmate_dir - Dependency directory to be coppied to the hoosegow
15
+ # image. This should include a file called
16
+ # `inmate.rb` that defines a Hoosegow::Inmate module.
17
+ # :image_name - The name of the Docker image to use. If this isn't
18
+ # specified, we will infer the image name from the
19
+ # hash the files present.
20
+ # :ruby_version - The Ruby version to install in the Docker
21
+ # container (Default RUBY_VERSION).
22
+ # :socket - Path to Unix socket where Docker daemon is
23
+ # running. (optional. defaults to
24
+ # "/var/run/docker.sock")
25
+ # :host - IP or hostname where Docker daemon is running.
26
+ # Don't set this if Docker is listening locally on a
27
+ # Unix socket.
28
+ # :port - TCP port where Docker daemon is running. Don't set
29
+ # this if Docker is listening locally on a Unix
30
+ # socket.
31
+ def initialize(options = {})
32
+ options = options.dup
33
+ @no_proxy = options.delete(:no_proxy)
34
+ @inmate_dir = options.delete(:inmate_dir) || '/hoosegow/inmate'
35
+ @image_name = options.delete(:image_name)
36
+ @ruby_version = options.delete(:ruby_version) || RUBY_VERSION
37
+ @docker_options = options
38
+ load_inmate_methods
39
+
40
+ # Don't want to have to require these in the container.
41
+ unless no_proxy?
42
+ require 'tmpdir'
43
+ require 'fileutils'
44
+ require 'open3'
45
+ require 'digest'
46
+ end
47
+ end
48
+
49
+ # Public: The thing that defines which files go into the docker image tarball.
50
+ def image_bundle
51
+ @image_bundle ||=
52
+ Hoosegow::ImageBundle.new.tap do |image|
53
+ image.add(File.expand_path('../../*', __FILE__), :ignore_hidden => true)
54
+ image.add(File.join(@inmate_dir, "*"), :prefix => 'inmate')
55
+ image.ruby_version = @ruby_version
56
+ end
57
+ end
58
+
59
+ # Public: Proxies method call to instance running in a Docker container.
60
+ #
61
+ # name - The method to call in the Docker instance.
62
+ # args - Arguments that should be passed to the Docker instance method.
63
+ # block - A block that can be yielded to.
64
+ #
65
+ # See docs/dispatch.md for more information.
66
+ #
67
+ # Returns the return value from the Docker instance method.
68
+ def proxy_send(name, args, &block)
69
+ proxy = Hoosegow::Protocol::Proxy.new(
70
+ :stdout => $stdout,
71
+ :stderr => $stderr,
72
+ :yield => block
73
+ )
74
+ encoded_send = proxy.encode_send(name, args)
75
+ docker.run_container(image_name, encoded_send) do |type, msg|
76
+ proxy.receive(type, msg)
77
+ end
78
+
79
+ proxy.return_value
80
+ end
81
+
82
+ # Public: Load inmate methods from #{inmate_dir}/inmate.rb and hook them up
83
+ # to proxied to the Docker container. If we are in the container, the methods
84
+ # are loaded and setup to be called directly.
85
+ #
86
+ # Returns nothing. Raises InmateImportError if there is a problem.
87
+ def load_inmate_methods
88
+ inmate_file = File.join @inmate_dir, 'inmate.rb'
89
+
90
+ unless File.exist?(inmate_file)
91
+ raise Hoosegow::InmateImportError, "inmate file doesn't exist"
92
+ end
93
+
94
+ require inmate_file
95
+
96
+ unless Hoosegow.const_defined?(:Inmate) && Hoosegow::Inmate.is_a?(Module)
97
+ raise Hoosegow::InmateImportError,
98
+ "inmate file doesn't define Hoosegow::Inmate"
99
+ end
100
+
101
+ if no_proxy?
102
+ self.extend Hoosegow::Inmate
103
+ else
104
+ inmate_methods = Hoosegow::Inmate.instance_methods
105
+ inmate_methods.each do |name|
106
+ define_singleton_method name do |*args, &block|
107
+ proxy_send name, args, &block
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # Public: We create/start a container after every run to reduce latency. This
114
+ # needs to be called before the process ends to cleanup that remaining
115
+ # container.
116
+ #
117
+ # Returns nothing.
118
+ def cleanup
119
+ docker.stop_container
120
+ docker.delete_container
121
+ end
122
+
123
+ # Check if the Docker image exists.
124
+ #
125
+ # Returns true/false.
126
+ def image_exists?
127
+ docker.image_exist? image_name
128
+ end
129
+
130
+ # Public: Build a Docker image from the Dockerfile in the root directory of
131
+ # the gem.
132
+ #
133
+ # Returns build output text. Raises ImageBuildError if there is a problem.
134
+ def build_image(&block)
135
+ docker.build_image image_name, image_bundle.tarball, &block
136
+ end
137
+
138
+ # Private: The name of the docker image to use. If not specified manually,
139
+ # this will be infered from the hash of the tarball.
140
+ #
141
+ # Returns string image name.
142
+ def image_name
143
+ @image_name || image_bundle.image_name
144
+ end
145
+
146
+ private
147
+ # Private: Get or create a Docker instance.
148
+ #
149
+ # Returns an Docker instance.
150
+ def docker
151
+ @docker ||= Docker.new @docker_options
152
+ end
153
+
154
+ # Returns true if we are in the Docker instance or are in develpment mode.
155
+ #
156
+ # Returns true/false.
157
+ def no_proxy?
158
+ !!@no_proxy
159
+ end
160
+ end