updated docs

pull/79/head
James Batt 4 years ago
parent 763675628e
commit 7d039ce89e

@ -3,9 +3,9 @@ name: Build and push docker images
on: on:
push: push:
branches: branches:
- "master" - 'master'
tags: tags:
- "v*.*.*" - 'v*.*.*'
jobs: jobs:
build: build:
@ -14,14 +14,14 @@ jobs:
- name: Prepare - name: Prepare
id: prep id: prep
run: | run: |
echo "building ref: $GITHUB_REF"
TAG=null TAG=null
IMAGE=place1/wg-access-server IMAGE=place1/wg-access-server
REF="${{ github.ref }}" if [[ "$GITHUB_REF" == refs/heads/master ]]; then
if [[ "$REF" == refs/heads/master ]]; then
TAG="$IMAGE:master" TAG="$IMAGE:master"
elif [[ "$REF" == refs/tags/* ]]; then elif [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${REF$refs/tags/}" VERSION="${REF#refs/tags/}"
TAG="$IMAGE:$VERSION,$IMAGE:latest" TAG="$IMAGE:$VERSION,$IMAGE:latest"
fi fi

@ -10,10 +10,16 @@
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "${workspaceFolder}/main.go", "program": "${workspaceFolder}/main.go",
"env": {}, "env": {
"WG_ADMIN_PASSWORD": "example",
"WG_WIREGUARD_PRIVATE_KEY": "4DRYOeSSeZyWRrLw357Pg9sv/RppMGwveTwz7sxM4mo=",
},
"args": [ "args": [
"serve", "serve",
"--config=config.yaml" "--config=config.yaml",
"--no-wireguard-enabled",
"--no-dns-enabled",
"--port=9001"
] ]
} }
] ]

@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.3.0-rc1] ## [next (v0.3.0)]
### Added ### Added
@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- the wireguard private key is now required when the storage backend is persistent (i.e. not `memory://`) - the wireguard private key is now required when the storage backend is persistent (i.e. not `memory://`)
- configuration flags, environment variables and file properties have been refactored for consistency
* all configuration file properties (excluding auth providers) can now be set via flags and environment variables
* all environment variables are prefixed with `WG_` to avoid collisions in hosted environments like Kubernetes
* all flags & environment variables are named consistently
* **breaking:** no functionality has been removed but you'll need to update any flags/envvars that you're using
### Deprecations ### Deprecations

@ -39,8 +39,8 @@ FROM alpine:3.10
RUN apk add iptables RUN apk add iptables
RUN apk add wireguard-tools RUN apk add wireguard-tools
RUN apk add curl RUN apk add curl
ENV CONFIG="/config.yaml" ENV WG_CONFIG="/config.yaml"
ENV STORAGE="sqlite3:///data/db.sqlite3" ENV WG_STORAGE="sqlite3:///data/db.sqlite3"
COPY --from=server /code/wg-access-server /usr/local/bin/wg-access-server COPY --from=server /code/wg-access-server /usr/local/bin/wg-access-server
COPY --from=website /code/build /website/build COPY --from=website /code/build /website/build
CMD ["wg-access-server", "serve"] CMD ["wg-access-server", "serve"]

@ -0,0 +1,8 @@
package cmd
// Command represents a wg-access-server
// subcommand module
type Command interface {
Name() string
Run()
}

@ -7,7 +7,7 @@ import (
"gopkg.in/alecthomas/kingpin.v2" "gopkg.in/alecthomas/kingpin.v2"
) )
func RegisterCommand(app *kingpin.Application) *migratecmd { func Register(app *kingpin.Application) *migratecmd {
cmd := &migratecmd{} cmd := &migratecmd{}
cli := app.Command(cmd.Name(), "Migrate your wg-access-server devices between storage backends. This tool is provided on a best effort bases.") cli := app.Command(cmd.Name(), "Migrate your wg-access-server devices between storage backends. This tool is provided on a best effort bases.")
cli.Arg("source", "The source storage URI").Required().StringVar(&cmd.src) cli.Arg("source", "The source storage URI").Required().StringVar(&cmd.src)

@ -4,8 +4,11 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"strings" "strings"
"github.com/docker/libnetwork/resolvconf"
"github.com/docker/libnetwork/types"
"github.com/place1/wg-access-server/internal/services" "github.com/place1/wg-access-server/internal/services"
"github.com/place1/wg-access-server/internal/storage" "github.com/place1/wg-access-server/internal/storage"
"github.com/place1/wg-access-server/pkg/authnz" "github.com/place1/wg-access-server/pkg/authnz"
@ -28,31 +31,31 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func RegisterCommand(app *kingpin.Application) *servecmd { func Register(app *kingpin.Application) *servecmd {
cmd := &servecmd{} cmd := &servecmd{}
cli := app.Command(cmd.Name(), "Run the server") cli := app.Command(cmd.Name(), "Run the server")
cli.Flag("config", "Path to a config file").Envar("CONFIG").StringVar(&cmd.configPath) cli.Flag("config", "Path to a wg-access-server config file").Envar("WG_CONFIG").FileVar(&cmd.ConfigFilePath)
cli.Flag("web-port", "The port that the web ui server will listen on").Envar("WEB_PORT").Default("8000").IntVar(&cmd.webPort) cli.Flag("admin-username", "Admin username (defaults to admin)").Envar("WG_ADMIN_USERNAME").Default("admin").StringVar(&cmd.AppConfig.AdminUsername)
cli.Flag("wireguard-port", "The port that the Wireguard server will listen on").Envar("WIREGUARD_PORT").Default("51820").IntVar(&cmd.wireguardPort) cli.Flag("admin-password", "Admin password (provide plaintext, stored in-memory only)").Envar("WG_ADMIN_PASSWORD").Required().StringVar(&cmd.AppConfig.AdminPassword)
cli.Flag("storage", "The storage backend connection string").Envar("STORAGE").Default("memory://").StringVar(&cmd.storage) cli.Flag("port", "The port that the web ui server will listen on").Envar("WG_PORT").Default("8000").IntVar(&cmd.AppConfig.Port)
cli.Flag("wireguard-private-key", "Wireguard private key").Envar("WIREGUARD_PRIVATE_KEY").StringVar(&cmd.privateKey) cli.Flag("external-host", "The external origin of the server (e.g. https://mydomain.com)").Envar("WG_EXTERNAL_HOST").StringVar(&cmd.AppConfig.ExternalHost)
cli.Flag("disable-metadata", "Disable metadata collection (i.e. metrics)").Envar("DISABLE_METADATA").Default("false").BoolVar(&cmd.disableMetadata) cli.Flag("storage", "The storage backend connection string").Envar("WG_STORAGE").Default("memory://").StringVar(&cmd.AppConfig.Storage)
cli.Flag("admin-username", "Admin username (defaults to admin)").Envar("ADMIN_USERNAME").Default("admin").StringVar(&cmd.adminUsername) cli.Flag("disable-metadata", "Disable metadata collection (i.e. metrics)").Envar("WG_DISABLE_METADATA").Default("false").BoolVar(&cmd.AppConfig.DisableMetadata)
cli.Flag("admin-password", "Admin password (provide plaintext, stored in-memory only)").Envar("ADMIN_PASSWORD").StringVar(&cmd.adminPassword) cli.Flag("wireguard-enabled", "Enable or disable the embedded wireguard server (useful for development)").Envar("WG_WIREGUARD_ENABLED").Default("true").BoolVar(&cmd.AppConfig.WireGuard.Enabled)
cli.Flag("upstream-dns", "An upstream DNS server to proxy DNS traffic to").Envar("UPSTREAM_DNS").StringVar(&cmd.upstreamDNS) cli.Flag("wireguard-interface", "Set the wireguard interface name").Default("wg0").Envar("WG_WIREGUARD_INTERFACE").StringVar(&cmd.AppConfig.WireGuard.Interface)
cli.Flag("wireguard-private-key", "Wireguard private key").Envar("WG_WIREGUARD_PRIVATE_KEY").StringVar(&cmd.AppConfig.WireGuard.PrivateKey)
cli.Flag("wireguard-port", "The port that the Wireguard server will listen on").Envar("WG_WIREGUARD_PORT").Default("51820").IntVar(&cmd.AppConfig.WireGuard.Port)
cli.Flag("vpn-cidr", "The network CIDR for the VPN").Envar("WG_VPN_CIDR").Default("10.44.0.0/24").StringVar(&cmd.AppConfig.VPN.CIDR)
cli.Flag("vpn-gateway-interface", "The gateway network interface (i.e. eth0)").Envar("WG_VPN_GATEWAY_INTERFACE").Default(detectDefaultInterface()).StringVar(&cmd.AppConfig.VPN.GatewayInterface)
cli.Flag("vpn-allowed-ips", "A list of networks that VPN clients will be allowed to connect to via the VPN").Envar("WG_VPN_ALLOWED_IPS").Default("0.0.0.0/1", "128.0.0.0/1").StringsVar(&cmd.AppConfig.VPN.AllowedIPs)
cli.Flag("dns-enabled", "Enable or disable the embedded dns proxy server (useful for development)").Envar("WG_DNS_ENABLED").Default("true").BoolVar(&cmd.AppConfig.DNS.Enabled)
cli.Flag("dns-upstream", "An upstream DNS server to proxy DNS traffic to. Defaults to resolveconf or 1.1.1.1").Envar("WG_DNS_UPSTREAM").Default(detectDNSUpstream()).StringsVar(&cmd.AppConfig.DNS.Upstream)
return cmd return cmd
} }
type servecmd struct { type servecmd struct {
configPath string ConfigFilePath *os.File
webPort int AppConfig config.AppConfig
wireguardPort int
storage string
privateKey string
disableMetadata bool
adminUsername string
adminPassword string
upstreamDNS string
} }
func (cmd *servecmd) Name() string { func (cmd *servecmd) Name() string {
@ -68,7 +71,7 @@ func (cmd *servecmd) Run() {
// WireGuard Server // WireGuard Server
wg := wgembed.NewNoOpInterface() wg := wgembed.NewNoOpInterface()
if conf.WireGuard.Enabled { if conf.WireGuard.Enabled {
wgimpl, err := wgembed.New(conf.WireGuard.InterfaceName) wgimpl, err := wgembed.New(conf.WireGuard.Interface)
if err != nil { if err != nil {
logrus.Fatal(errors.Wrap(err, "failed to create wireguard interface")) logrus.Fatal(errors.Wrap(err, "failed to create wireguard interface"))
} }
@ -91,7 +94,7 @@ func (cmd *servecmd) Run() {
logrus.Infof("wireguard VPN network is %s", conf.VPN.CIDR) logrus.Infof("wireguard VPN network is %s", conf.VPN.CIDR)
if err := network.ConfigureForwarding(conf.WireGuard.InterfaceName, conf.VPN.GatewayInterface, conf.VPN.CIDR, conf.VPN.AllowedIPs); err != nil { if err := network.ConfigureForwarding(conf.WireGuard.Interface, conf.VPN.GatewayInterface, conf.VPN.CIDR, conf.VPN.AllowedIPs); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -160,13 +163,6 @@ func (cmd *servecmd) Run() {
// Static website // Static website
site.PathPrefix("/").Handler(services.WebsiteRouter()) site.PathPrefix("/").Handler(services.WebsiteRouter())
// publicRouter.NotFoundHandler = authMiddleware.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// if authsession.Authenticated(r.Context()) {
// router.ServeHTTP(w, r)
// } else {
// http.Redirect(w, r, "/signin", http.StatusTemporaryRedirect)
// }
// }))
publicRouter := router publicRouter := router
// Listen // Listen
@ -184,107 +180,92 @@ func (cmd *servecmd) Run() {
} }
func (cmd *servecmd) ReadConfig() *config.AppConfig { func (cmd *servecmd) ReadConfig() *config.AppConfig {
// here we're filling out the config struct if cmd.ConfigFilePath != nil {
// with values from our flags/defaults. defer cmd.ConfigFilePath.Close()
config := &config.AppConfig{} if b, err := ioutil.ReadAll(cmd.ConfigFilePath); err == nil {
config.Port = cmd.webPort if err := yaml.Unmarshal(b, &cmd.AppConfig); err != nil {
config.WireGuard.InterfaceName = "wg0"
config.WireGuard.Port = cmd.wireguardPort
config.VPN.CIDR = "10.44.0.0/24"
config.DisableMetadata = cmd.disableMetadata
config.WireGuard.Enabled = true
config.WireGuard.PrivateKey = cmd.privateKey
config.Storage = cmd.storage
config.VPN.AllowedIPs = []string{"0.0.0.0/0"}
config.DNS.Enabled = true
config.AdminPassword = cmd.adminPassword
config.AdminSubject = cmd.adminUsername
if cmd.upstreamDNS != "" {
config.DNS.Upstream = []string{cmd.upstreamDNS}
}
if cmd.configPath != "" {
if b, err := ioutil.ReadFile(cmd.configPath); err == nil {
if err := yaml.Unmarshal(b, &config); err != nil {
logrus.Fatal(errors.Wrap(err, "failed to bind configuration file")) logrus.Fatal(errors.Wrap(err, "failed to bind configuration file"))
} }
} }
} }
if config.LogLevel != "" { if cmd.AppConfig.LogLevel != "" {
level, err := logrus.ParseLevel(config.LogLevel) if level, err := logrus.ParseLevel(cmd.AppConfig.LogLevel); err == nil {
if err != nil { logrus.SetLevel(level)
logrus.Fatal(errors.Wrap(err, "invalid log level - should be one of fatal, error, warn, info, debug, trace"))
} }
logrus.SetLevel(level)
} }
if config.DisableMetadata { if cmd.AppConfig.DisableMetadata {
logrus.Info("Metadata collection has been disabled. No metrics or device connectivity information will be recorded or shown") logrus.Info("Metadata collection has been disabled. No metrics or device connectivity information will be recorded or shown")
} }
if config.VPN.GatewayInterface == "" { // set a basic auth entry for the admin user
iface, err := defaultInterface() if cmd.AppConfig.Auth.Basic == nil {
if err != nil { cmd.AppConfig.Auth.Basic = &authconfig.BasicAuthConfig{}
logrus.Warn(errors.Wrap(err, "failed to set default value for VPN.GatewayInterface")) }
} else { pw, err := bcrypt.GenerateFromPassword([]byte(cmd.AppConfig.AdminPassword), bcrypt.DefaultCost)
config.VPN.GatewayInterface = iface if err != nil {
} logrus.Fatal(errors.Wrap(err, "failed to generate a bcrypt hash for the provided admin password"))
} }
cmd.AppConfig.Auth.Basic.Users = append(cmd.AppConfig.Auth.Basic.Users, fmt.Sprintf("%s:%s", cmd.AppConfig.AdminUsername, string(pw)))
if config.WireGuard.PrivateKey == "" { // we'll generate a private key when using memory://
if !strings.HasPrefix(config.Storage, "memory://") { // storage only.
if cmd.AppConfig.WireGuard.PrivateKey == "" {
if !strings.HasPrefix(cmd.AppConfig.Storage, "memory://") {
logrus.Fatal(missingPrivateKey) logrus.Fatal(missingPrivateKey)
} }
key, err := wgtypes.GeneratePrivateKey() key, err := wgtypes.GeneratePrivateKey()
if err != nil { if err != nil {
logrus.Fatal(errors.Wrap(err, "failed to generate a server private key")) logrus.Fatal(errors.Wrap(err, "failed to generate a server private key"))
} }
config.WireGuard.PrivateKey = key.String() cmd.AppConfig.WireGuard.PrivateKey = key.String()
}
if config.AdminPassword != "" && config.AdminSubject != "" {
if config.Auth.Basic == nil {
config.Auth.Basic = &authconfig.BasicAuthConfig{}
}
// htpasswd.AcceptBcrypt(config.AdminPassword)
pw, err := bcrypt.GenerateFromPassword([]byte(config.AdminPassword), bcrypt.DefaultCost)
if err != nil {
logrus.Fatal(errors.Wrap(err, "failed to generate a bcrypt hash for the provided admin password"))
}
config.Auth.Basic.Users = append(config.Auth.Basic.Users, fmt.Sprintf("%s:%s", config.AdminSubject, string(pw)))
} }
return config return &cmd.AppConfig
} }
func claimsMiddleware(conf *config.AppConfig) authsession.ClaimsMiddleware { func claimsMiddleware(conf *config.AppConfig) authsession.ClaimsMiddleware {
return func(user *authsession.Identity) error { return func(user *authsession.Identity) error {
if user.Subject == conf.AdminSubject { if user.Subject == conf.AdminUsername {
user.Claims.Add("admin", "true") user.Claims.Add("admin", "true")
} }
return nil return nil
} }
} }
func defaultInterface() (string, error) { func detectDNSUpstream() string {
upstream := []string{}
if r, err := resolvconf.Get(); err == nil {
upstream = resolvconf.GetNameservers(r.Content, types.IPv4)
}
if len(upstream) == 0 {
logrus.Warn("failed to get nameservers from /etc/resolv.conf defaulting to 1.1.1.1 for DNS instead")
upstream = []string{"1.1.1.1"}
}
return upstream[0]
}
func detectDefaultInterface() string {
links, err := netlink.LinkList() links, err := netlink.LinkList()
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to list network interfaces") logrus.Warn(errors.Wrap(err, "failed to list network interfaces"))
return ""
} }
for _, link := range links { for _, link := range links {
routes, err := netlink.RouteList(link, 4) routes, err := netlink.RouteList(link, 4)
if err != nil { if err != nil {
return "", errors.Wrapf(err, "failed to list routes for interface %s", link.Attrs().Name) logrus.Warn(errors.Wrapf(err, "failed to list routes for interface %s", link.Attrs().Name))
return ""
} }
for _, route := range routes { for _, route := range routes {
if route.Dst == nil { if route.Dst == nil {
return link.Attrs().Name, nil return link.Attrs().Name
} }
} }
} }
return "", errors.New("could not determine the default network interface name") logrus.Warn(errors.New("could not determine the default network interface name"))
return ""
} }
var missingPrivateKey = `missing wireguard private key: var missingPrivateKey = `missing wireguard private key:

Binary file not shown.

@ -13,9 +13,9 @@ services:
- "wg-access-server-data:/data" - "wg-access-server-data:/data"
# - "./config.yaml:/config.yaml" # if you have a custom config file # - "./config.yaml:/config.yaml" # if you have a custom config file
environment: environment:
- "ADMIN_USERNAME=admin" - "WG_ADMIN_USERNAME=admin"
- "ADMIN_PASSWORD=${ADMIN_PASSWORD:?\n\nplease set the ADMIN_PASSWORD environment variable:\n export ADMIN_PASSWORD=example\n}" - "WG_ADMIN_PASSWORD=${WG_ADMIN_PASSWORD:?\n\nplease set the WG_ADMIN_PASSWORD environment variable:\n export WG_ADMIN_PASSWORD=example\n}"
- "WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY:?\n\nplease set the WIREGUARD_PRIVATE_KEY environment variable:\n export WIREGUARD_PRIVATE_KEY=$(wg genkey)\n}" - "WG_WIREGUARD_PRIVATE_KEY=${WG_WIREGUARD_PRIVATE_KEY:?\n\nplease set the WG_WIREGUARD_PRIVATE_KEY environment variable:\n export WG_WIREGUARD_PRIVATE_KEY=$(wg genkey)\n}"
ports: ports:
- "8000:8000/tcp" - "8000:8000/tcp"
- "51820:51820/udp" - "51820:51820/udp"

@ -0,0 +1,53 @@
# Configuration
You can configure wg-access-server using environment variables, cli flags or a config file
taking precedence over one another in that order.
The default configuration should work out of the box if you're just looking to try it out.
The only required configuration is an admin password and a wireguard private key. The admin
password can be anything you like. You can generate a wireguard private key by
[following the official docs](https://www.wireguard.com/quickstart/#key-generation).
TLDR:
```bash
wg genkey
```
The config file format is `yaml` and an example is provided [below](#the-config-file-configyaml).
Here's what you can configure:
| Environment Variable | CLI Flag | Config File Path | Required | Default (docker) | Description |
| -------------------------- | ------------------------- | ---------------------- | -------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WG_CONFIG` | `--config` | `loglevel` | | `info` | Global log level |
| `WG_ADMIN_USERNAME` | `--admin-username` | `adminUsername` | | `admin` | The admin account username |
| `WG_ADMIN_PASSWORD` | `--admin-password` | `adminPassword` | Yes | | The admin account password |
| `WG_PORT` | `--port` | `port` | | `8000` | The port the web ui will listen on (http) |
| `WG_EXTERNAL_HOST` | `--external-host` | `externalHost` | | | The external domain for the server (e.g. https://www.mydomain.com) |
| `WG_STORAGE` | `--storage` | `storage` | | `sqlite3:///data/db.sqlite3` | A storage backend connection string. See [storage docs](./3-storage.md) |
| `WG_DISABLE_METADATA` | `--disable-metadata` | `disableMetadata` | | `false` | Turn off collection of device metadata logging. Includes last handshake time and RX/TX bytes only. |
| `WG_WIREGUARD_ENABLED` | `--wireguard-enabled` | `wireguard.enabled` | | `true` | Enable/disable the wireguard server. Useful for development on non-linux machines. |
| `WG_WIREGUARD_INTERFACE` | `--wireguard-interface` | `wireguard.interface` | | `wg0` | The wireguard network interface name |
| `WG_WIREGUARD_PRIVATE_KEY` | `--wireguard-private-key` | `wireguard.privateKey` | Yes | | The wireguard private key. This value is required and must be stable. If this value changes all devices must re-register. |
| `WG_WIREGUARD_PORT` | `--wireguard-port` | `wireguard.port` | | `51820` | The wireguard server port (udp) |
| `WG_VPN_CIDR` | `--vpn-cidr` | `vpn.cidr` | | `10.44.0.0/24` | The VPN network range. VPN clients will be assigned IP addresses in this range. |
| `WG_VPN_GATEWAY_INTERFACE` | `--vpn-gateway-interface` | `vpn.gatewayInterface` | | _default gateway interface (e.g. eth0)_ | The VPN gateway interface. VPN client traffic will be forwarded to this interface. |
| `WG_VPN_ALLOWED_IPS` | `--vpn-allowed-ips` | `vpn.allowedIPs` | | `0.0.0.0/1, 128.0.0.0/1` | Allowed IPs that clients may route through this VPN. This will be set in the client's WireGuard connection file and routing is also enforced by the server using iptables. |
| `WG_DNS_ENABLED` | `--dns-enabled` | `dns.enabled` | | `true` | Enable/disable the embedded DNS proxy server. This is enabled by default and allows VPN clients to avoid DNS leaks by sending all DNS requests to wg-access-server itself. |
| `WG_DNS_UPSTREAM` | `--dns-upstream` | `dns.upstream` | | _resolveconf autodetection or 1.1.1.1_ | The upstream DNS server to proxy DNS requests to. By default the host machine's resolveconf configuration is used to find it's upstream DNS server, otherwise 1.1.1.1 (cloudflare) is used. |
## The Config File (config.yaml)
Here's an example config file to get started with.
```yaml
loglevel: info
storage: sqlite3:///data/db.sqlite3
wireguard:
privateKey: "<some-key>"
dns:
upstream:
- "8.8.8.8"
```

@ -1,13 +1,13 @@
# Storage Backends # Storage
wg-access-server supports 4 storage backends suitable for different use-cases. wg-access-server supports 4 storage backends.
| Backend | Persistent | Supports HA | Use Case | | Backend | Persistent | Supports HA | Use Case |
|----------|------------|-------------|----------------------------------------| | -------- | ---------- | ----------- | ---------------------------------------- |
| memory | ❌ | ❌ | Local development | | memory | ❌ | ❌ | Local development |
| sqlite3 | ✔️ | ❌ | Production - single instance deployments | | sqlite3 | ✔️ | ❌ | Production - single instance deployments |
| postgres | ✔️ | ✔️ (soon) | Production - multi instance deployments | | postgres | ✔️ | ✔️ (soon) | Production - multi instance deployments |
| mysql | ✔️ | ❌ | Production - single instance deployments | | mysql | ✔️ | ❌ | Production - single instance deployments |
## Backends ## Backends

@ -0,0 +1,75 @@
# Authentication
Authentication is pluggable in wg-access-server. Community contributions are welcome
for supporting new authentication backends.
If you're just getting started you can skip over this section and rely on the default
admin account instead.
If your authentication system is not yet supported and you aren't quite ready to
contribute you could try using a project like [dex](https://github.com/dexidp/dex)
or SaaS provider like [Auth0](https://auth0.com/) which supports a wider variety of
authentication protocols. wg-access-server can happily be an OpenID Connect client
to a larger solution like this.
The following authentication backends are currently supported:
| Backend | Use Case | Notes |
| -------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| Basic Auth | Deployments with a static list of users. Simple and great for self-hosters and home use-cases | The wg-access-server admin account is powered by this backend |
| OpenID Connect | For delegating authentication to an existing identity solution | |
| Gitlab | For delegating authentication to gitlab. Supports self-hosted Gitlab. | |
## Configuration
Currently authentication providers are only configurable via the wg-access-server
config file (config.yaml).
Below is an annotated example config section that can be used as a starting point.
```yaml
# Configure zero or more authentication backends
auth:
# HTTP Basic Authentication
basic:
# Users is a list of htpasswd encoded username:password pairs
# supports BCrypt, Sha, Ssha, Md5
# You can create a user using "htpasswd -nB <username>"
users: []
oidc:
# A name for the backend (can be anything you want)
name: "My OIDC Backend"
# Should point to the OIDC Issuer (excluding /.well-known/openid-configuration)
issuer: "https://identity.example.com"
# Your OIDC client credentials which would be provided by your OIDC provider
clientID: "<client-id>"
clientSecret: "<client-secret>"
# List of scopes to request defaults to ["openid"]
scopes:
- openid
# The full redirect URL
# The path can be almost anything as long as it doesn't
# conflict with a path that the web UI uses.
# /callback is recommended.
redirectURL: "https://wg-access-server.example.com/callback"
# You can optionally restrict access to users with an email address
# that matches an allowed domain.
# If empty or omitted then all email domains will be allowed.
emailDomains:
- example.com
# This is an advanced feature that allows you to define
# OIDC claim mapping expressions.
# This feature is used to define wg-access-server admins
# based off a claim in your OIDC token
# See https://github.com/Knetic/govaluate/blob/9aa49832a739dcd78a5542ff189fb82c3e423116/MANUAL.md for how to write rules
userClaimsRules:
admin: "'WireguardAdmins' in group_membership"
gitlab:
name: "My Gitlab Backend"
baseURL: "https://mygitlab.example.com"
clientID: "<client-id>"
clientSecret: "<client-secret>"
redirectURL: "https:///wg-access-server.example.com/callback"
emailDomains:
- example.com
```

@ -1,136 +0,0 @@
# Configuration
## Environment Variables
| Variable | Description |
|-----------------------|-------------|
| CONFIG | Set the config file path |
| WIREGUARD_PRIVATE_KEY | Set the wireguard private key |
| STORAGE | Set the directory where device config will be persisted |
| ADMIN_USERNAME | Set the username (subject) for the admin account |
| ADMIN_PASSWORD | Set the admin account's password. The admin account will be a basic-auth user. Leave blank if your admin username authenticates via a configured authentication backend. |
| UPSTREAM_DNS | Set the upstream DNS server to proxy client DNS requests to. If empty, resolv.conf will be respected. |
| LOG_LEVEL | Set the server's log level (debug, **info**, error, critical) |
| DISABLE_METADATA | If true, the server will not record device level metadata such as the last handshake time, tx/rx data size |
## CLI Flags
All environment variables can be configured via a
CLI flag as well.
For example you can configure `STORAGE` by passing `--storage="<value>"`.
## Config File (config.yaml)
Here's an annotated config file example:
```yaml
# The application's log level.
# Can be debug, info, error
# Optional, defaults to info
loglevel: info
# Disable device metadata storage.
# Device metadata includes the last handshake time,
# total sent/received bytes count, their endpoint IP.
# This metadata is captured from wireguard itself.
# Disabling this flag will not stop wireguard from capturing
# this data.
# See stored data here: https://github.com/Place1/wg-access-server/blob/master/internal/storage/contracts.go#L14
# Optional, defaults to false.
disableMetadata: false
# The port that the web ui server (http) will listen on.
# Optional, defaults to 8000
port: 8000
# Directory that VPN devices (WireGuard peers)
# What type of storage do you want? inmemory (default), file:///some/directory, or postgres, mysql, sqlite3
storage: "memory://"
wireguard:
# The network interface name for wireguard
# Optional, defaults to wg0
interfaceName: wg0
# The WireGuard PrivateKey
# You can generate this value using "$ wg genkey"
# If this value is empty then the server will use an in-memory
# generated key
privateKey: ""
# ExternalAddress is the address (without port) that clients use to connect to the wireguard interface
# By default, this will be empty and the web ui
# will use the current page's origin i.e. window.location.origin
# Optional
externalHost: ""
# The WireGuard ListenPort
# Optional, defaults to 51820
port: 51820
vpn:
# CIDR configures a network address space
# that client (WireGuard peers) will be allocated
# an IP address from.
# Optional
cidr: "10.44.0.0/24"
# GatewayInterface will be used in iptable forwarding
# rules that send VPN traffic from clients to this interface
# Most use-cases will want this interface to have access
# to the outside internet
# If not configured then the server will select the default
# network interface e.g. eth0
# Optional
gatewayInterface: ""
# The "AllowedIPs" for VPN clients.
# This value will be included in client config
# files and in server-side iptable rules
# to enforce network access.
# Optional
allowedIPs:
- "0.0.0.0/0"
dns:
# Enable a DNS proxy for VPN clients.
# Optional, Defaults to true
enabled: true
# upstream DNS servers.
# that the server-side DNS proxy will forward requests to.
# By default /etc/resolv.conf will be used to find upstream
# DNS servers.
# Optional
upstream:
- "1.1.1.1"
# Auth configures optional authentication backends
# to controll access to the web ui.
# Devices will be managed on a per-user basis if any
# auth backends are configured.
# If no authentication backends are configured then
# the server will not require any authentication.
# It's recommended to make use of basic authentication
# or use an upstream HTTP proxy that enforces authentication
# Optional
auth:
# HTTP Basic Authentication
basic:
# Users is a list of htpasswd encoded username:password pairs
# supports BCrypt, Sha, Ssha, Md5
# You can create a user using "htpasswd -nB <username>"
users: []
oidc:
name: "" # anything you want
issuer: "" # Should point to the oidc url without .well-known
clientID: ""
clientSecret: ""
scopes: null # list of scopes, defaults to ["openid"]
redirectURL: "" # full url you want the oidc to redirect to, example: https://vpn-admin.example.com/finish-signin
# See https://github.com/Knetic/govaluate/blob/9aa49832a739dcd78a5542ff189fb82c3e423116/MANUAL.md for how to write rules
userClaimsRules:
admin: "'WireguardAdmins' in group_membership"
# Optionally restrict login to users with an allowed email domain
# if empty or omitted, any email domain will be allowed.
emailDomains:
- example.com
gitlab:
name: ""
baseURL: ""
clientID: ""
clientSecret: ""
redirectURL: ""
# Optionally restrict login to users with an allowed email domain
# if empty or omitted, any email domain will be allowed.
emailDomains:
- example.com
```

@ -3,7 +3,7 @@
You can run wg-access-server using the following example You can run wg-access-server using the following example
docker Docker Compose file. docker Docker Compose file.
Checkout the [configuration docs](../configuration.md) to learn how wg-access-server Checkout the [configuration docs](../2-configuration.md) to learn how wg-access-server
can be configured. can be configured.
```yaml ```yaml

@ -12,7 +12,6 @@ require (
github.com/docker/libnetwork v0.8.0-dev.2.0.20200217033114-6659f7f4d8c1 github.com/docker/libnetwork v0.8.0-dev.2.0.20200217033114-6659f7f4d8c1
github.com/golang/protobuf v1.4.2 github.com/golang/protobuf v1.4.2
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.7.4 github.com/gorilla/mux v1.7.4
github.com/gorilla/sessions v1.2.0 github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect

@ -72,8 +72,6 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@ -140,10 +138,6 @@ 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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/place1/wg-embed v0.2.0 h1:JzdDeDDWqzY7w6/JJ/fW+q/Qc7AzI/i1GqyeFNf/HT0=
github.com/place1/wg-embed v0.2.0/go.mod h1:i09dm8AEkurC4oATFxjvyH0+e1pdmtZoNk2FfPupROI=
github.com/place1/wg-embed v0.3.0 h1:n7piTgnp3MgyceBEAD/A7ZiLA4kH8qkqCTVPLBHj6SE=
github.com/place1/wg-embed v0.3.0/go.mod h1:i09dm8AEkurC4oATFxjvyH0+e1pdmtZoNk2FfPupROI=
github.com/place1/wg-embed v0.4.0 h1:rToHj4+TuI2ruv2mz3Y16vvisv280BuzdojsGGNQ/pM= github.com/place1/wg-embed v0.4.0 h1:rToHj4+TuI2ruv2mz3Y16vvisv280BuzdojsGGNQ/pM=
github.com/place1/wg-embed v0.4.0/go.mod h1:i09dm8AEkurC4oATFxjvyH0+e1pdmtZoNk2FfPupROI= github.com/place1/wg-embed v0.4.0/go.mod h1:i09dm8AEkurC4oATFxjvyH0+e1pdmtZoNk2FfPupROI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

@ -5,24 +5,41 @@ import (
) )
type AppConfig struct { type AppConfig struct {
LogLevel string `yaml:"loglevel"` // Set the log level.
DisableMetadata bool `yaml:"disableMetadata"` // Defaults to "info" (fatal, error, warn, info, debug, trace)
AdminSubject string `yaml:"adminSubject"` LogLevel string `yaml:"loglevel"`
AdminPassword string `yaml:"adminPassword"` // Set the superadmin username
// Defaults to "admin"
AdminUsername string `yaml:"adminUsername"`
// Set the superadmin password (required)
AdminPassword string `yaml:"adminPassword"`
// Port sets the port that the web UI will listen on. // Port sets the port that the web UI will listen on.
// Defaults to 8000 // Defaults to 8000
Port int `yaml:"port"` Port int `yaml:"port"`
// ExternalAddress is the address that clients
// use to connect to the wireguard interface
// By default, this will be empty and the web ui
// will use the current page's origin.
ExternalHost string `yaml:"externalHost"`
// The storage backend where device configuration will // The storage backend where device configuration will
// be persisted. // be persisted.
// Supports memory:// file:// postgres:// mysql:// sqlite3:// // Supports memory:// file:// postgres:// mysql:// sqlite3://
// Defaults to memory:// // Defaults to memory://
Storage string `yaml:"storage"` Storage string `yaml:"storage"`
// DisableMetadata allows you to turn off collection of device
// metadata including last handshake time & rx/tx bytes
DisableMetadata bool `yaml:"disableMetadata"`
// Configure WireGuard related settings
WireGuard struct { WireGuard struct {
// Set this to false to disable the embedded wireguard
// server. This is useful for development environments
// on mac and windows where we don't currently support
// the OS's network stack.
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
// The network interface name of the WireGuard // The network interface name of the WireGuard
// network device. // network device.
// Defaults to wg0 // Defaults to wg0
InterfaceName string `yaml:"interfaceName"` Interface string `yaml:"interface"`
// The WireGuard PrivateKey // The WireGuard PrivateKey
// If this value is lost then any existing // If this value is lost then any existing
// clients (WireGuard peers) will no longer // clients (WireGuard peers) will no longer
@ -31,15 +48,11 @@ type AppConfig struct {
// their connection configuration or setup // their connection configuration or setup
// their VPN again using the web ui (easier for most people) // their VPN again using the web ui (easier for most people)
PrivateKey string `yaml:"privateKey"` PrivateKey string `yaml:"privateKey"`
// ExternalAddress is the address that clients
// use to connect to the wireguard interface
// By default, this will be empty and the web ui
// will use the current page's origin.
ExternalHost *string `yaml:"externalHost"`
// The WireGuard ListenPort // The WireGuard ListenPort
// Defaults to 51820 // Defaults to 51820
Port int `yaml:"port"` Port int `yaml:"port"`
} `yaml:"wireguard"` } `yaml:"wireguard"`
// Configure VPN related settings (networking)
VPN struct { VPN struct {
// CIDR configures a network address space // CIDR configures a network address space
// that client (WireGuard peers) will be allocated // that client (WireGuard peers) will be allocated
@ -56,8 +69,9 @@ type AppConfig struct {
// files and in server-side iptable rules // files and in server-side iptable rules
// to enforce network access. // to enforce network access.
// defaults to ["0.0.0.0/1", "128.0.0.0/1"] // defaults to ["0.0.0.0/1", "128.0.0.0/1"]
AllowedIPs []string `yaml:"AllowedIPs"` AllowedIPs []string `yaml:"allowedIPs"`
} }
// Configure the embeded DNS server
DNS struct { DNS struct {
// Enabled allows you to turn on/off // Enabled allows you to turn on/off
// the VPN DNS proxy feature. // the VPN DNS proxy feature.

@ -7,8 +7,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/docker/libnetwork/resolvconf"
"github.com/docker/libnetwork/types"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -27,23 +25,12 @@ type DNSServer struct {
} }
func New(opts DNSServerOpts) (*DNSServer, error) { func New(opts DNSServerOpts) (*DNSServer, error) {
if len(opts.Upstream) == 0 {
upstream := opts.Upstream return nil, errors.New("at least 1 upstream dns server is required for the dns proxy server to function")
if len(upstream) == 0 {
if r, err := resolvconf.Get(); err == nil {
upstream = resolvconf.GetNameservers(r.Content, types.IPv4)
}
}
if len(upstream) == 0 {
logrus.Warn("failed to get nameservers from /etc/resolv.conf defaulting to 1.1.1.1 for DNS instead")
upstream = append(upstream, "1.1.1.1")
} }
addr := "0.0.0.0:53" addr := "0.0.0.0:53"
logrus.Infof("starting dns server on %s with upstreams: %s", addr, strings.Join(opts.Upstream, ", "))
logrus.Infof("starting dns server on %s with upstreams: %s", addr, strings.Join(upstream, ", "))
dnsServer := &DNSServer{ dnsServer := &DNSServer{
server: &dns.Server{ server: &dns.Server{
@ -55,7 +42,7 @@ func New(opts DNSServerOpts) (*DNSServer, error) {
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
}, },
cache: cache.New(10*time.Minute, 10*time.Minute), cache: cache.New(10*time.Minute, 10*time.Minute),
upstream: upstream, upstream: opts.Upstream,
} }
dnsServer.server.Handler = dnsServer dnsServer.server.Handler = dnsServer

@ -33,7 +33,7 @@ func (s *ServerService) Info(ctx context.Context, req *proto.InfoReq) (*proto.In
} }
return &proto.InfoRes{ return &proto.InfoRes{
Host: stringValue(s.Config.WireGuard.ExternalHost), Host: stringValue(&s.Config.ExternalHost),
PublicKey: publicKey, PublicKey: publicKey,
Port: int32(s.Config.WireGuard.Port), Port: int32(s.Config.WireGuard.Port),
HostVpnIp: network.ServerVPNIP(s.Config.VPN.CIDR).IP.String(), HostVpnIp: network.ServerVPNIP(s.Config.VPN.CIDR).IP.String(),

@ -7,6 +7,7 @@ import (
"runtime" "runtime"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/place1/wg-access-server/cmd"
"github.com/place1/wg-access-server/cmd/migrate" "github.com/place1/wg-access-server/cmd/migrate"
"github.com/place1/wg-access-server/cmd/serve" "github.com/place1/wg-access-server/cmd/serve"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -15,20 +16,24 @@ import (
var ( var (
app = kingpin.New("wg-access-server", "An all-in-one WireGuard Access Server & VPN solution") app = kingpin.New("wg-access-server", "An all-in-one WireGuard Access Server & VPN solution")
logLevel = app.Flag("log-level", "Log level (debug, info, error)").Envar("LOG_LEVEL").Default("info").String() logLevel = app.Flag("log-level", "Log level: trace, debug, info, error, fatal").Envar("LOG_LEVEL").Default("info").String()
servecmd = serve.RegisterCommand(app)
migratecmd = migrate.RegisterCommand(app)
) )
func main() { func main() {
cmd := kingpin.MustParse(app.Parse(os.Args[1:])) // all the subcommands for wg-access-server
commands := []cmd.Command{
serve.Register(app),
migrate.Register(app),
}
// parse CLI arguments
clicmd := kingpin.MustParse(app.Parse(os.Args[1:]))
// set global log level
level, err := logrus.ParseLevel(*logLevel) level, err := logrus.ParseLevel(*logLevel)
if err != nil { if err != nil {
logrus.Fatal(errors.Wrap(err, "invalid log level - should be one of fatal, error, warn, info, debug, trace")) logrus.Fatal(errors.Wrap(err, "invalid log level - should be one of fatal, error, warn, info, debug, trace"))
} }
logrus.SetLevel(level) logrus.SetLevel(level)
logrus.SetReportCaller(true) logrus.SetReportCaller(true)
logrus.SetFormatter(&logrus.TextFormatter{ logrus.SetFormatter(&logrus.TextFormatter{
@ -37,12 +42,10 @@ func main() {
}, },
}) })
switch cmd { for _, c := range commands {
case servecmd.Name(): if clicmd == c.Name() {
servecmd.Run() c.Run()
case migratecmd.Name(): return
migratecmd.Run() }
default:
logrus.Fatal(fmt.Errorf("unknown command: %s", cmd))
} }
} }

Loading…
Cancel
Save