<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -13,11 +13,76 @@ chance that you'll accidentally lock yourself out.
 === Instructions
 
 * &lt;tt&gt;script/plugin install git://github.com/railsmachine/moonshine_ssh.git&lt;/tt&gt;
-* Use &lt;tt&gt;configure&lt;/tt&gt; to customize plugin settings if desired:
-    configure(:ssh =&gt; {
-      :port =&gt; '9022', 
-      :allow_users =&gt; ['rob','rails'] 
-    })
-* Include the plugin and recipe(s) you want to use in your Moonshine manifest.
+* Edit moonshine.yml to customize plugin settings if desired:
+    :ssh
+      :port: 9022
+      :allow_users:
+        - rob
+        - rails
+* Include the plugin and recipe you in your manifest:
     plugin :ssh
-    recipe :ssh
\ No newline at end of file
+    recipe :ssh
+    
+=== SFTP-only users
+
+OpenSSH supports chrooting users natively. You can create users who will be chrooted
+and only allowed to use SFTP (no console access) by adding the following to your 
+moonshine.yml:
+
+    :ssh:
+      :sftponly: true
+
+Then add this to your manifest:
+
+    plugin :ssh
+    recipe :ssh
+  
+This creates a user called sftponly with a randomized password. To allow access, you can manag authorized_keys through your manifest:
+
+    file '/home/sftponly/home/sftponly/.ssh/authorized_keys',
+      :ensure =&gt; :present,
+      :content =&gt; YOUR_SSH_PUBKEYS
+
+Once connected via sftp, the user will be chrooted to /home/sftponly where they will only see a 
+'home' directory. The user can upload files to /home/sftponly. For a normal user, the uploaded 
+files will be located at /home/sftponly/home/sftponly.
+
+==== Advanced
+
+For a more complicated example, we'll consider a user called 'rob' who needs to upload 
+files into a directory under the Rails application's shared/ directory. Since he will 
+be chrooted, he can't have direct access to the directory. Also, the directory is owned 
+by the rails group, so he'll need to be a member of that. Finally, we don't want to worry 
+about public keys, so he'll need a password.
+
+In your moonshine.yml:
+
+    :ssh:
+      :sftponly:
+        :users: 
+          :rob:
+            :groups: rails
+            :password: sooper_sekrit
+
+In your manifest:
+
+    plugin :ssh
+    recipe :ssh
+    def mount_assets
+      file '/home/rob/home/rob/assets', 
+        :ensure =&gt; :directory, 
+        :owner =&gt; 'rob',
+        :require =&gt; file('/home/rob/home/rob')
+        
+      mount '/home/rob/home/rob/assets',
+        :ensure =&gt; :mounted,
+        :device =&gt; &quot;#{configuration[:deploy_to]}/shared/assets/&quot;,
+        :options =&gt; 'bind',
+        :fstype =&gt; :none,
+        :atboot =&gt; true,
+        :remounts =&gt; true,
+        :require =&gt; file('/home/rob/home/rob/assets')
+    end
+    recipe :mount_assets
+
+Then deploy, and you're done! The user's /home/rob/assets directory is now actually the shared assets directory. Anything uploaded there will be available to the application automatically.
\ No newline at end of file</diff>
      <filename>README.rdoc</filename>
    </modified>
    <modified>
      <diff>@@ -8,9 +8,15 @@ module SSH
   #   configure(:ssh =&gt; {:permit_root_login =&gt; 'yes', :port =&gt; 9022})
   #
   def ssh(options = {})
+
     package 'ssh', :ensure =&gt; :installed
     service 'ssh', :enable =&gt; true, :ensure =&gt; :running
 
+    if options[:sftponly]
+      options[:subsystem] = {:sftp =&gt; 'internal-sftp'}
+      sftponly(options[:sftponly])
+    end
+
     file '/etc/ssh/sshd_config.new',
       :mode =&gt; '644',
       :content =&gt; template(File.join(File.dirname(__FILE__), '..', 'templates', 'sshd_config'), binding),
@@ -23,6 +29,61 @@ module SSH
       :refreshonly =&gt; true,
       :require =&gt; file('/etc/ssh/sshd_config.new'),
       :notify =&gt; service('ssh')
+    
+  end
+  
+  private 
+  
+  # Sets up users and directories for chrooted, sftp-only access
+  # By default, the chroot is /home/USERNAME and the user's home
+  # will be inside that, at /home/USERNAME/home/USERNAME
+  def sftponly(options)
+    
+    group 'sftponly', :ensure =&gt; :present
+    
+    exec &quot;fake shell&quot;,
+      :command =&gt; &quot;echo /bin/false &gt;&gt; /etc/shells&quot;,
+      :onlyif =&gt; &quot;test -z `grep /bin/false /etc/shells`&quot;
+    
+    (options[:users]||'sftponly').to_a.each do |user,hash|
+      user = user.to_s
+      
+      chroot = options[:chroot_directory] || &quot;/home/#{user}&quot;
+      homedir = chroot + ( hash[:home] || &quot;/home/#{user}&quot; )
+      
+      parent_directories homedir, :owner =&gt; 'root', :mode =&gt; '755'
+      file homedir, 
+        :ensure =&gt; :directory, 
+        :owner =&gt; user, 
+        :group =&gt; user, 
+        :require =&gt; user(user)
+      
+      user user, 
+        :ensure =&gt; :present,
+        :home =&gt; &quot;/home/#{user}/home/#{user}&quot;,
+        :shell =&gt; &quot;/bin/false&quot;,
+        :groups =&gt; (['sftponly'] + hash[:groups].to_a).uniq,
+        :require =&gt; [group('sftponly'),exec('fake shell')],
+        :notify =&gt; exec(&quot;#{user} password&quot;)
+      
+      password = hash[:password] || rand_pass(6)
+      exec &quot;#{user} password&quot;,
+        :command =&gt; &quot;echo #{user}:#{password} | chpasswd&quot;,
+        :refreshonly =&gt; true
+
+    end
+  end
+  
+  def parent_directories(path,options)
+    options.merge!({:ensure =&gt; :directory})
+    while path != &quot;/&quot;
+      path = File.split(path)[0]
+      file path, options
+    end
+  end
+  
+  def rand_pass(len)
+    Array.new(len/2) { rand(256) }.pack('C*').unpack('H*').first
   end
   
 end
\ No newline at end of file</diff>
      <filename>lib/ssh.rb</filename>
    </modified>
    <modified>
      <diff>@@ -38,5 +38,49 @@ describe SSH do
     @manifest.ssh( :port =&gt; 9022 )
     @manifest.files[&quot;/etc/ssh/sshd_config.new&quot;].content.should match /Port 9022/
   end
+  
+  describe &quot;configured for sftponly&quot; do
+    
+    before do
+      @manifest.ssh(:sftponly =&gt; {
+        :users =&gt; {
+          :rob =&gt; {
+            :password =&gt; 'sekrit',
+            :groups =&gt; 'rails'
+          }
+        }
+      })
+    end
+    
+    it &quot;should create the sftponly group&quot; do
+      @manifest.groups.should include('sftponly')
+    end
+    
+    it &quot;should add the user&quot; do
+      @manifest.users.should include('rob')
+      @manifest.execs['rob password'].command.should == &quot;echo rob:sekrit | chpasswd&quot;
+    end
+    
+    it &quot;should add user to sftponly and extra groups if requested&quot; do
+      @manifest.users['rob'].groups.should == ['sftponly','rails']
+    end
+    
+    it &quot;should create the home directory&quot; do
+      @manifest.files.should include('/home/rob/home/rob')
+      @manifest.files['/home/rob/home/rob'].owner.should == 'rob'
+      @manifest.files['/home/rob/home'].owner.should == 'root'
+      @manifest.files['/home/rob'].owner.should == 'root'
+    end
+   
+    it &quot;should set the sftp subsystem&quot; do
+      @manifest.files['/etc/ssh/sshd_config.new'].content.should match /Subsystem sftp internal-sftp/
+    end
+   
+    it &quot;should add a group matcher to the sshd config&quot; do
+      @manifest.files['/etc/ssh/sshd_config.new'].content.should match /Match Group sftponly/
+      @manifest.files['/etc/ssh/sshd_config.new'].content.should match /ChrootDirectory \/home\/%u/
+    end
+    
+  end
     
 end
\ No newline at end of file</diff>
      <filename>spec/ssh_spec.rb</filename>
    </modified>
    <modified>
      <diff>@@ -69,19 +69,28 @@ X11Forwarding &lt;%= options[:x11_forwarding] || 'yes' %&gt;
 X11DisplayOffset &lt;%= options[:x11_display_offsets] || 10 %&gt;
 PrintMotd &lt;%= options[:print_motd] || 'no' %&gt;
 PrintLastLog &lt;%= options[:print_last_log] || 'yes' %&gt;
-TCPKeepAlive yes
+TCPKeepAlive &lt;%= options[:tcp_keep_alive] || 'yes' %&gt;
 #UseLogin no
 
 # http://techgurulive.com/2008/09/15/how-to-protect-ssh-from-multiple-and-parallel-coordinated-attacks/
 MaxStartups &lt;%= options[:max_startups] || '2:50:5' %&gt;
-#Banner /etc/issue.net
+
+&lt;% if options[:use_banner] %&gt;
+Banner /etc/issue.net
+&lt;% end %&gt;
 
 # Allow client to pass locale environment variables
 AcceptEnv LANG LC_*
 
+&lt;% if options[:subsystem] %&gt;
+  &lt;% options[:subsystem].each do |subsystem,command| %&gt;
+Subsystem &lt;%= subsystem %&gt; &lt;%= command %&gt;
+  &lt;% end %&gt;
+&lt;% else %&gt;
 Subsystem sftp /usr/lib/openssh/sftp-server
+&lt;% end %&gt;
 
-UsePAM yes
+UsePAM &lt;%= options[:use_pam] || 'yes' %&gt;
 
 &lt;% if options[:allow_users] %&gt;
 AllowUsers &lt;%= options[:allow_users].to_a.join(' ') %&gt;
@@ -90,4 +99,12 @@ AllowUsers &lt;%= options[:allow_users].to_a.join(' ') %&gt;
 AllowGroups &lt;%= options[:allow_groups].to_a.join(' ') %&gt;
 &lt;% end %&gt;
 
-&lt;%= options[:extra] %&gt;
\ No newline at end of file
+&lt;%= options[:extra] %&gt;
+
+&lt;% if options[:sftponly] %&gt;
+Match Group sftponly
+  ChrootDirectory &lt;%= options[:sftponly][:chroot_directory] || '/home/%u' %&gt;
+  ForceCommand internal-sftp
+  X11Forwarding no
+  AllowTcpForwarding no
+&lt;% end %&gt;
\ No newline at end of file</diff>
      <filename>templates/sshd_config</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>2bcb4213149bbd8f73cd6ffadf4c9d7a96e010ac</id>
    </parent>
  </parents>
  <author>
    <name>Rob Lingle</name>
    <email>rob@actsasif.com</email>
  </author>
  <url>http://github.com/railsmachine/moonshine_ssh/commit/a761b37ab6c124f47346531891e189ef2fee336d</url>
  <id>a761b37ab6c124f47346531891e189ef2fee336d</id>
  <committed-date>2009-07-07T11:33:55-07:00</committed-date>
  <authored-date>2009-07-07T11:33:55-07:00</authored-date>
  <message>super easy chrooted, sftp-only users</message>
  <tree>26ff29f45e1bbe079dd5d685a6ccea7e753265b8</tree>
  <committer>
    <name>Rob Lingle</name>
    <email>rob@actsasif.com</email>
  </committer>
</commit>
