diff --git a/Dockerfile b/Dockerfile index 0cd61478..8afc1301 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,6 +36,8 @@ ENV VPNSP=pia \ OPENVPN_ROOT=no \ OPENVPN_TARGET_IP= \ TZ= \ + UID=1000 \ + GID=1000 \ # PIA only PASSWORD= \ REGION="Austria" \ @@ -82,8 +84,8 @@ HEALTHCHECK --interval=3m --timeout=3s --start-period=20s --retries=1 CMD /entry RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables unbound tinyproxy tzdata && \ echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ apk add -q --progress --no-cache --update shadowsocks-libev && \ - rm -rf /*.zip /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-anchor /usr/sbin/unbound-checkconf /usr/sbin/unbound-control /usr/sbin/unbound-control-setup /usr/sbin/unbound-host /etc/tinyproxy/tinyproxy.conf && \ - adduser nonrootuser -D -H --uid 1000 && \ - chown nonrootuser -R /etc/unbound /etc/tinyproxy && \ - chmod 700 /etc/unbound /etc/tinyproxy -COPY --from=builder --chown=1000:1000 /tmp/gobuild/entrypoint /entrypoint \ No newline at end of file + rm -rf /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-* /etc/tinyproxy/tinyproxy.conf && \ + deluser openvpn && \ + deluser tinyproxy && \ + deluser unbound +COPY --from=builder /tmp/gobuild/entrypoint /entrypoint diff --git a/README.md b/README.md index 085c7021..61c648ac 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,8 @@ docker run --rm --network=container:pia alpine:3.11 wget -qO- https://ipinfo.io | `OPENVPN_TARGET_IP` | | (Optional) Specify a target VPN server IP address to use, valid for Mullvad and Private Internet Access | | `OPENVPN_CIPHER` | | Specify a custom cipher to use, use at your own risk. It will also set `ncp-disable` if using AES GCM for PIA | | `OPENVPN_AUTH` | | Specify a custom auth algorithm to use (i.e. `sha256`) *for pia only* | +| `UID` | `1000` | User ID to run as non root and for ownership of files written | +| `GID` | `1000` | Group ID to run as non root and for ownership of files written | ## Connect to it diff --git a/cmd/main.go b/cmd/main.go index 7cc718d6..a716964b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,6 +13,7 @@ import ( "github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/network" "github.com/qdm12/golibs/signals" + "github.com/qdm12/private-internet-access-docker/internal/alpine" "github.com/qdm12/private-internet-access-docker/internal/constants" "github.com/qdm12/private-internet-access-docker/internal/dns" "github.com/qdm12/private-internet-access-docker/internal/env" @@ -30,10 +31,6 @@ import ( "github.com/qdm12/private-internet-access-docker/internal/windscribe" ) -const ( - uid, gid = 1000, 1000 -) - func main() { logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1) if err != nil { @@ -52,6 +49,7 @@ func main() { client := network.NewClient(15 * time.Second) // Create configurators fileManager := files.NewFileManager() + alpineConf := alpine.NewConfigurator(logger, fileManager) ovpnConf := openvpn.NewConfigurator(logger, fileManager) dnsConf := dns.NewConfigurator(logger, client, fileManager) firewallConf := firewall.NewConfigurator(logger, fileManager) @@ -74,6 +72,13 @@ func main() { e.FatalOnError(err) logger.Info(allSettings.String()) + err = alpineConf.CreateUser("nonrootuser", allSettings.UID) + e.FatalOnError(err) + err = fileManager.SetOwnership("/etc/unbound", allSettings.UID, allSettings.GID) + e.FatalOnError(err) + err = fileManager.SetOwnership("/etc/tinyproxy", allSettings.UID, allSettings.GID) + e.FatalOnError(err) + if err := ovpnConf.CheckTUN(); err != nil { logger.Warn(err) err = ovpnConf.CreateTUN() @@ -92,7 +97,7 @@ func main() { openVPNUser = allSettings.Windscribe.User openVPNPassword = allSettings.Windscribe.Password } - err = ovpnConf.WriteAuthFile(openVPNUser, openVPNPassword, uid, gid) + err = ovpnConf.WriteAuthFile(openVPNUser, openVPNPassword, allSettings.UID, allSettings.GID) e.FatalOnError(err) // Temporarily reset chain policies allowing Kubernetes sidecar to @@ -113,11 +118,11 @@ func main() { if allSettings.DNS.Enabled { initialDNSToUse := constants.DNSProviderMapping()[allSettings.DNS.Providers[0]] dnsConf.UseDNSInternally(initialDNSToUse.IPs[0]) - err = dnsConf.DownloadRootHints(uid, gid) + err = dnsConf.DownloadRootHints(allSettings.UID, allSettings.GID) e.FatalOnError(err) - err = dnsConf.DownloadRootKey(uid, gid) + err = dnsConf.DownloadRootKey(allSettings.UID, allSettings.GID) e.FatalOnError(err) - err = dnsConf.MakeUnboundConf(allSettings.DNS, uid, gid) + err = dnsConf.MakeUnboundConf(allSettings.DNS, allSettings.UID, allSettings.GID) e.FatalOnError(err) stream, waitFn, err := dnsConf.Start(allSettings.DNS.VerbosityDetailsLevel) e.FatalOnError(err) @@ -135,19 +140,54 @@ func main() { var connections []models.OpenVPNConnection switch allSettings.VPNSP { case "pia": - connections, err = piaConf.GetOpenVPNConnections(allSettings.PIA.Region, allSettings.OpenVPN.NetworkProtocol, allSettings.PIA.Encryption, allSettings.OpenVPN.TargetIP) + connections, err = piaConf.GetOpenVPNConnections( + allSettings.PIA.Region, + allSettings.OpenVPN.NetworkProtocol, + allSettings.PIA.Encryption, + allSettings.OpenVPN.TargetIP) e.FatalOnError(err) - err = piaConf.BuildConf(connections, allSettings.PIA.Encryption, allSettings.OpenVPN.Verbosity, uid, gid, allSettings.OpenVPN.Root, allSettings.OpenVPN.Cipher, allSettings.OpenVPN.Auth) + err = piaConf.BuildConf( + connections, + allSettings.PIA.Encryption, + allSettings.OpenVPN.Verbosity, + allSettings.UID, + allSettings.GID, + allSettings.OpenVPN.Root, + allSettings.OpenVPN.Cipher, + allSettings.OpenVPN.Auth) e.FatalOnError(err) case "mullvad": - connections, err = mullvadConf.GetOpenVPNConnections(allSettings.Mullvad.Country, allSettings.Mullvad.City, allSettings.Mullvad.ISP, allSettings.OpenVPN.NetworkProtocol, allSettings.Mullvad.Port, allSettings.OpenVPN.TargetIP) + connections, err = mullvadConf.GetOpenVPNConnections( + allSettings.Mullvad.Country, + allSettings.Mullvad.City, + allSettings.Mullvad.ISP, + allSettings.OpenVPN.NetworkProtocol, + allSettings.Mullvad.Port, + allSettings.OpenVPN.TargetIP) e.FatalOnError(err) - err = mullvadConf.BuildConf(connections, allSettings.OpenVPN.Verbosity, uid, gid, allSettings.OpenVPN.Root, allSettings.OpenVPN.Cipher) + err = mullvadConf.BuildConf( + connections, + allSettings.OpenVPN.Verbosity, + allSettings.UID, + allSettings.GID, + allSettings.OpenVPN.Root, + allSettings.OpenVPN.Cipher) e.FatalOnError(err) case "windscribe": - connections, err = windscribeConf.GetOpenVPNConnections(allSettings.Windscribe.Region, allSettings.OpenVPN.NetworkProtocol, allSettings.Windscribe.Port, allSettings.OpenVPN.TargetIP) + connections, err = windscribeConf.GetOpenVPNConnections( + allSettings.Windscribe.Region, + allSettings.OpenVPN.NetworkProtocol, + allSettings.Windscribe.Port, + allSettings.OpenVPN.TargetIP) e.FatalOnError(err) - err = windscribeConf.BuildConf(connections, allSettings.OpenVPN.Verbosity, uid, gid, allSettings.OpenVPN.Root, allSettings.OpenVPN.Cipher, allSettings.OpenVPN.Auth) + err = windscribeConf.BuildConf( + connections, + allSettings.OpenVPN.Verbosity, + allSettings.UID, + allSettings.GID, + allSettings.OpenVPN.Root, + allSettings.OpenVPN.Cipher, + allSettings.OpenVPN.Auth) e.FatalOnError(err) } @@ -167,7 +207,13 @@ func main() { e.FatalOnError(err) if allSettings.TinyProxy.Enabled { - err = tinyProxyConf.MakeConf(allSettings.TinyProxy.LogLevel, allSettings.TinyProxy.Port, allSettings.TinyProxy.User, allSettings.TinyProxy.Password, uid, gid) + err = tinyProxyConf.MakeConf( + allSettings.TinyProxy.LogLevel, + allSettings.TinyProxy.Port, + allSettings.TinyProxy.User, + allSettings.TinyProxy.Password, + allSettings.UID, + allSettings.GID) e.FatalOnError(err) err = firewallConf.AllowAnyIncomingOnPort(allSettings.TinyProxy.Port) e.FatalOnError(err) @@ -182,7 +228,11 @@ func main() { } if allSettings.ShadowSocks.Enabled { - err = shadowsocksConf.MakeConf(allSettings.ShadowSocks.Port, allSettings.ShadowSocks.Password, uid, gid) + err = shadowsocksConf.MakeConf( + allSettings.ShadowSocks.Port, + allSettings.ShadowSocks.Password, + allSettings.UID, + allSettings.GID) e.FatalOnError(err) err = firewallConf.AllowAnyIncomingOnPort(allSettings.ShadowSocks.Port) e.FatalOnError(err) @@ -202,7 +252,11 @@ func main() { if err != nil { logger.Error("port forwarding:", err) } - if err := piaConf.WritePortForward(allSettings.PIA.PortForwarding.Filepath, port); err != nil { + if err := piaConf.WritePortForward( + allSettings.PIA.PortForwarding.Filepath, + port, + allSettings.UID, + allSettings.GID); err != nil { logger.Error("port forwarding:", err) } if err := piaConf.AllowPortForwardFirewall(constants.TUN, port); err != nil { diff --git a/go.mod b/go.mod index b14a17e4..c551b3d1 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/kyokomi/emoji v2.1.0+incompatible - github.com/qdm12/golibs v0.0.0-20200224235252-bc16caae82ea - github.com/stretchr/testify v1.4.0 - golang.org/x/sys v0.0.0-20190412213103-97732733099d + github.com/qdm12/golibs v0.0.0-20200329231626-f55b47cd4e96 + github.com/stretchr/testify v1.5.1 + golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 ) diff --git a/go.sum b/go.sum index baada8dc..9ea327ec 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/qdm12/golibs v0.0.0-20200224235252-bc16caae82ea h1:ILUt8UU795dKZ2r7p3G44w1/vcGM/FUFXCcePYXYfS8= -github.com/qdm12/golibs v0.0.0-20200224235252-bc16caae82ea/go.mod h1:YULaFjj6VGmhjak6f35sUWwEleHUmngN5IQ3kdvd6XE= +github.com/qdm12/golibs v0.0.0-20200329231626-f55b47cd4e96 h1:vGvPItljtw8Z0xVJSyE80Z+6zzRZqrHoXr5vx5iB+rI= +github.com/qdm12/golibs v0.0.0-20200329231626-f55b47cd4e96/go.mod h1:YULaFjj6VGmhjak6f35sUWwEleHUmngN5IQ3kdvd6XE= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -70,6 +70,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= @@ -93,6 +95,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 h1:TC0v2RSO1u2kn1ZugjrFXkRZAEaqMN/RW+OTZkBzmLE= +golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= diff --git a/internal/alpine/alpine.go b/internal/alpine/alpine.go new file mode 100644 index 00000000..a54e5fd5 --- /dev/null +++ b/internal/alpine/alpine.go @@ -0,0 +1,28 @@ +package alpine + +import ( + "os/user" + + "github.com/qdm12/golibs/files" + "github.com/qdm12/golibs/logging" +) + +const logPrefix = "alpine configurator" + +type Configurator interface { + CreateUser(username string, uid int) error +} + +type configurator struct { + fileManager files.FileManager + lookupUID func(uid string) (*user.User, error) + lookupUser func(username string) (*user.User, error) +} + +func NewConfigurator(logger logging.Logger, fileManager files.FileManager) Configurator { + return &configurator{ + fileManager: fileManager, + lookupUID: user.LookupId, + lookupUser: user.Lookup, + } +} diff --git a/internal/alpine/users.go b/internal/alpine/users.go new file mode 100644 index 00000000..0c30d81e --- /dev/null +++ b/internal/alpine/users.go @@ -0,0 +1,38 @@ +package alpine + +import ( + "fmt" + "os/user" +) + +// CreateUser creates a user in Alpine with the given UID +func (c *configurator) CreateUser(username string, uid int) error { + UIDStr := fmt.Sprintf("%d", uid) + u, err := c.lookupUID(UIDStr) + _, unknownUID := err.(user.UnknownUserIdError) + if err != nil && !unknownUID { + return fmt.Errorf("cannot create user: %w", err) + } else if u != nil { + if u.Username == username { + return nil + } + return fmt.Errorf("user with ID %d exists with username %q instead of %q", uid, u.Username, username) + } + u, err = c.lookupUser(username) + _, unknownUsername := err.(user.UnknownUserError) + if err != nil && !unknownUsername { + return fmt.Errorf("cannot create user: %w", err) + } else if u != nil { + return fmt.Errorf("cannot create user: user with name %s already exists for ID %s instead of %d", username, u.Uid, uid) + } + passwd, err := c.fileManager.ReadFile("/etc/passwd") + if err != nil { + return fmt.Errorf("cannot create user: %w", err) + } + passwd = append(passwd, []byte(fmt.Sprintf("%s:x:%d:::/dev/null:/sbin/nologin\n", username, uid))...) + + if err := c.fileManager.WriteToFile("/etc/passwd", passwd); err != nil { + return fmt.Errorf("cannot create user: %w", err) + } + return nil +} diff --git a/internal/params/ids.go b/internal/params/ids.go new file mode 100644 index 00000000..335806fc --- /dev/null +++ b/internal/params/ids.go @@ -0,0 +1,15 @@ +package params + +import ( + libparams "github.com/qdm12/golibs/params" +) + +// GetUID obtains the user ID to use from the environment variable UID +func (p *paramsReader) GetUID() (uid int, err error) { + return p.envParams.GetEnvIntRange("UID", 0, 65535, libparams.Default("1000")) +} + +// GetGID obtains the group ID to use from the environment variable GID +func (p *paramsReader) GetGID() (gid int, err error) { + return p.envParams.GetEnvIntRange("GID", 0, 65535, libparams.Default("1000")) +} diff --git a/internal/params/params.go b/internal/params/params.go index 4250ef11..00560272 100644 --- a/internal/params/params.go +++ b/internal/params/params.go @@ -28,6 +28,10 @@ type ParamsReader interface { GetDNSOverTLSPrivateAddresses() (privateAddresses []string) GetDNSOverTLSIPv6() (ipv6 bool, err error) + // IDs + GetUID() (uid int, err error) + GetGID() (gid int, err error) + // Firewall getters GetExtraSubnets() (extraSubnets []net.IPNet, err error) diff --git a/internal/pia/pia.go b/internal/pia/pia.go index 23f0e200..162c66f6 100644 --- a/internal/pia/pia.go +++ b/internal/pia/pia.go @@ -20,7 +20,7 @@ type Configurator interface { encryption models.PIAEncryption, targetIP net.IP) (connections []models.OpenVPNConnection, err error) BuildConf(connections []models.OpenVPNConnection, encryption models.PIAEncryption, verbosity, uid, gid int, root bool, cipher, auth string) (err error) GetPortForward() (port uint16, err error) - WritePortForward(filepath models.Filepath, port uint16) (err error) + WritePortForward(filepath models.Filepath, port uint16, uid, gid int) (err error) AllowPortForwardFirewall(device models.VPNDevice, port uint16) (err error) } diff --git a/internal/pia/portforward.go b/internal/pia/portforward.go index cb24eacb..7cc0f21a 100644 --- a/internal/pia/portforward.go +++ b/internal/pia/portforward.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/qdm12/golibs/files" "github.com/qdm12/private-internet-access-docker/internal/constants" "github.com/qdm12/private-internet-access-docker/internal/models" ) @@ -35,9 +36,13 @@ func (c *configurator) GetPortForward() (port uint16, err error) { return body.Port, nil } -func (c *configurator) WritePortForward(filepath models.Filepath, port uint16) (err error) { +func (c *configurator) WritePortForward(filepath models.Filepath, port uint16, uid, gid int) (err error) { c.logger.Info("%s: Writing forwarded port to %s", logPrefix, filepath) - return c.fileManager.WriteLinesToFile(string(filepath), []string{fmt.Sprintf("%d", port)}) + return c.fileManager.WriteLinesToFile( + string(filepath), + []string{fmt.Sprintf("%d", port)}, + files.Ownership(uid, gid), + files.Permissions(400)) } func (c *configurator) AllowPortForwardFirewall(device models.VPNDevice, port uint16) (err error) { diff --git a/internal/settings/settings.go b/internal/settings/settings.go index aeca8c38..d1a1ef61 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -18,6 +18,8 @@ type Settings struct { Firewall Firewall TinyProxy TinyProxy ShadowSocks ShadowSocks + UID int + GID int } func (s *Settings) String() string { @@ -32,6 +34,7 @@ func (s *Settings) String() string { } return strings.Join([]string{ "Settings summary below:", + fmt.Sprintf("|-- Using UID %d and GID %d", s.UID, s.GID), s.OpenVPN.String(), vpnServiceProvider, s.DNS.String(), @@ -115,5 +118,13 @@ func GetAllSettings(params params.ParamsReader) (settings Settings, err error) { if err != nil { return settings, err } + settings.UID, err = params.GetUID() + if err != nil { + return settings, err + } + settings.GID, err = params.GetGID() + if err != nil { + return settings, err + } return settings, nil }