Skip to content

Commit

Permalink
support serving the API over unix domain socket
Browse files Browse the repository at this point in the history
`unix://$PATH` as listen argument will bind aptly to a unix domain socket
rather than TCP.

This allows binding the API to a UDS rather than a port.
Since aptly has no concept of authentication or any amount of high level
API hardening one needs to bottle it up in some other manner. Binding
to a localhost port is often a step in the right direction, ultimately is
still a scary insecure setup as any user on that host getting compromised
would mean that the entire archive is compromised as well.
UDS on the other hand are basically files and have their access managed
by regular file permission. As such, binding to a socket is in fact
the least insecure way to listen as you'd have to explicitly open up the
socket permissions to an access qualified group. In the most conservative
scenario that means no one but the aptly user can talk to the API, in a
more practical setup apache might get access as well and proxy the UDS
with authentication or limited to GET operations.

Using UDS allows reducing the attack surface of the API server while
preserving all the flexibility.
  • Loading branch information
hsitter committed Feb 28, 2017
1 parent f86e6eb commit dbee214
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -23,7 +23,7 @@ before_install:
- . env/bin/activate
- pip install six packaging appdirs
- pip install -U pip setuptools
- pip install boto requests python-swiftclient
- pip install boto requests requests-unixsocket python-swiftclient
- mkdir -p $GOPATH/src/github.com/smira
- ln -s $TRAVIS_BUILD_DIR $GOPATH/src/github.com/smira || true
- cd $GOPATH/src/github.com/smira/aptly
Expand Down
29 changes: 26 additions & 3 deletions cmd/api_serve.go
Expand Up @@ -2,11 +2,15 @@ package cmd

import (
"fmt"
"net"
"net/http"
"net/url"
"os"

"github.com/smira/aptly/api"
"github.com/smira/aptly/utils"
"github.com/smira/commander"
"github.com/smira/flag"
"net/http"
)

func aptlyAPIServe(cmd *commander.Command, args []string) error {
Expand Down Expand Up @@ -34,6 +38,22 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {

fmt.Printf("\nStarting web server at: %s (press Ctrl+C to quit)...\n", listen)

listenURL, err := url.Parse(listen)
if err == nil && listenURL.Scheme == "unix" {
file := listenURL.Path
os.Remove(file)
listener, err := net.Listen("unix", file)
if err != nil {
return fmt.Errorf("failed to listen on: %s\n%s", file, err)
}
defer listener.Close()
err = http.Serve(listener, api.Router(context))
if err != nil {
return fmt.Errorf("unable to serve: %s", err)
}
return nil
}

err = http.ListenAndServe(listen, api.Router(context))
if err != nil {
return fmt.Errorf("unable to serve: %s", err)
Expand All @@ -48,16 +68,19 @@ func makeCmdAPIServe() *commander.Command {
UsageLine: "serve",
Short: "start API HTTP service",
Long: `
Stat HTTP server with aptly REST API.
Start HTTP server with aptly REST API. The server can listen to either a port
or Unix domain socket. When using a socket, Aptly will fully manage the socket
file.
Example:
$ aptly api serve -listen=:8080
$ aptly api serve -listen=unix:///tmp/aptly.sock
`,
Flag: *flag.NewFlagSet("aptly-serve", flag.ExitOnError),
}

cmd.Flag.String("listen", ":8080", "host:port for HTTP listening")
cmd.Flag.String("listen", ":8080", "host:port for HTTP listening or unix://path to listen on a Unix domain socket")
cmd.Flag.Bool("no-lock", false, "don't lock the database")

return cmd
Expand Down
12 changes: 6 additions & 6 deletions man/aptly.1
Expand Up @@ -1685,20 +1685,17 @@ host:port for HTTP listening
\fBaptly\fR \fBapi\fR \fBserve\fR
.
.P
Stat HTTP server with aptly REST API\.
Start HTTP server with aptly REST API\. The server can listen to either a port or Unix domain socket\. When using a socket, Aptly will fully manage the socket file\.
.
.P
Example:
.
.P
$ aptly api serve \-listen=:8080
Example: $ aptly api serve \-listen=:8080 $ aptly api serve \-listen=unix:///tmp/aptly\.sock
.
.P
Options:
.
.TP
\-\fBlisten\fR=:8080
host:port for HTTP listening
host:port for HTTP listening or unix://path to listen on a Unix domain socket
.
.TP
\-\fBno\-lock\fR=false
Expand Down Expand Up @@ -1882,5 +1879,8 @@ Harald Sitter (https://github\.com/apachelogger)
.IP "\[ci]" 4
Johannes Layher (https://github\.com/jola5)
.
.IP "\[ci]" 4
Charles Hsu (https://github\.com/charz)
.
.IP "" 0

1 change: 1 addition & 0 deletions system/t12_api/__init__.py
Expand Up @@ -9,3 +9,4 @@
from .graph import *
from .snapshots import *
from .packages import *
from .unix_socket import *
36 changes: 36 additions & 0 deletions system/t12_api/unix_socket.py
@@ -0,0 +1,36 @@
import requests_unixsocket
import time
import urllib

from lib import BaseTest

class UnixSocketAPITest(BaseTest):
aptly_server = None
socket_path = "/tmp/_aptly_test.sock"
base_url = ("unix://%s" % socket_path)

def prepare(self):
if self.aptly_server is None:
self.aptly_server = self._start_process("aptly api serve -no-lock -listen=%s" % (self.base_url),)
time.sleep(1)
super(UnixSocketAPITest, self).prepare()

def shutdown(self):
if self.aptly_server is not None:
self.aptly_server.terminate()
self.aptly_server.wait()
self.aptly_server = None
super(UnixSocketAPITest, self).shutdown()

def run(self):
pass

"""
Verify we can listen on a unix domain socket.
"""
def check(self):
session = requests_unixsocket.Session()
r = session.get('http+unix://%s/api/version' % urllib.quote(UnixSocketAPITest.socket_path, safe=''))
# Just needs to come back, we actually don't care much about the code.
# Only needs to verify that the socket is actually responding.
self.check_equal(r.json(), {'Version': '0.9.8~dev'})

0 comments on commit dbee214

Please sign in to comment.