diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 04d27fb..ca22b90 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -3,9 +3,9 @@ name: Build and push docker images on: push: branches: - - "master" + - 'master' tags: - - "v*.*.*" + - 'v*.*.*' jobs: build: @@ -14,14 +14,14 @@ jobs: - name: Prepare id: prep run: | + echo "building ref: $GITHUB_REF" + TAG=null IMAGE=place1/wg-access-server - REF="${{ github.ref }}" - - if [[ "$REF" == refs/heads/master ]]; then + if [[ "$GITHUB_REF" == refs/heads/master ]]; then TAG="$IMAGE:master" - elif [[ "$REF" == refs/tags/* ]]; then - VERSION="${REF$refs/tags/}" + elif [[ "$GITHUB_REF" == refs/tags/* ]]; then + VERSION="${REF#refs/tags/}" TAG="$IMAGE:$VERSION,$IMAGE:latest" fi diff --git a/.vscode/launch.json b/.vscode/launch.json index 53d7904..a25f86e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,10 +10,16 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}/main.go", - "env": {}, + "env": { + "WG_ADMIN_PASSWORD": "example", + "WG_WIREGUARD_PRIVATE_KEY": "4DRYOeSSeZyWRrLw357Pg9sv/RppMGwveTwz7sxM4mo=", + }, "args": [ "serve", - "--config=config.yaml" + "--config=config.yaml", + "--no-wireguard-enabled", + "--no-dns-enabled", + "--port=9001" ] } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index db966dd..69abc35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [v0.3.0-rc1] +## [next (v0.3.0)] ### Added @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - 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 diff --git a/Dockerfile b/Dockerfile index fc92099..faa4f73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,8 +39,8 @@ FROM alpine:3.10 RUN apk add iptables RUN apk add wireguard-tools RUN apk add curl -ENV CONFIG="/config.yaml" -ENV STORAGE="sqlite3:///data/db.sqlite3" +ENV WG_CONFIG="/config.yaml" +ENV WG_STORAGE="sqlite3:///data/db.sqlite3" COPY --from=server /code/wg-access-server /usr/local/bin/wg-access-server COPY --from=website /code/build /website/build CMD ["wg-access-server", "serve"] diff --git a/cmd/contracts.go b/cmd/contracts.go new file mode 100644 index 0000000..042ef95 --- /dev/null +++ b/cmd/contracts.go @@ -0,0 +1,8 @@ +package cmd + +// Command represents a wg-access-server +// subcommand module +type Command interface { + Name() string + Run() +} diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go index 14a1970..7300d90 100644 --- a/cmd/migrate/main.go +++ b/cmd/migrate/main.go @@ -7,7 +7,7 @@ import ( "gopkg.in/alecthomas/kingpin.v2" ) -func RegisterCommand(app *kingpin.Application) *migratecmd { +func Register(app *kingpin.Application) *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.Arg("source", "The source storage URI").Required().StringVar(&cmd.src) diff --git a/cmd/serve/main.go b/cmd/serve/main.go index fe94520..c065f0e 100644 --- a/cmd/serve/main.go +++ b/cmd/serve/main.go @@ -4,8 +4,11 @@ import ( "fmt" "io/ioutil" "net/http" + "os" "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/storage" "github.com/place1/wg-access-server/pkg/authnz" @@ -28,31 +31,31 @@ import ( "github.com/sirupsen/logrus" ) -func RegisterCommand(app *kingpin.Application) *servecmd { +func Register(app *kingpin.Application) *servecmd { cmd := &servecmd{} cli := app.Command(cmd.Name(), "Run the server") - cli.Flag("config", "Path to a config file").Envar("CONFIG").StringVar(&cmd.configPath) - cli.Flag("web-port", "The port that the web ui server will listen on").Envar("WEB_PORT").Default("8000").IntVar(&cmd.webPort) - cli.Flag("wireguard-port", "The port that the Wireguard server will listen on").Envar("WIREGUARD_PORT").Default("51820").IntVar(&cmd.wireguardPort) - cli.Flag("storage", "The storage backend connection string").Envar("STORAGE").Default("memory://").StringVar(&cmd.storage) - cli.Flag("wireguard-private-key", "Wireguard private key").Envar("WIREGUARD_PRIVATE_KEY").StringVar(&cmd.privateKey) - cli.Flag("disable-metadata", "Disable metadata collection (i.e. metrics)").Envar("DISABLE_METADATA").Default("false").BoolVar(&cmd.disableMetadata) - cli.Flag("admin-username", "Admin username (defaults to admin)").Envar("ADMIN_USERNAME").Default("admin").StringVar(&cmd.adminUsername) - cli.Flag("admin-password", "Admin password (provide plaintext, stored in-memory only)").Envar("ADMIN_PASSWORD").StringVar(&cmd.adminPassword) - cli.Flag("upstream-dns", "An upstream DNS server to proxy DNS traffic to").Envar("UPSTREAM_DNS").StringVar(&cmd.upstreamDNS) + cli.Flag("config", "Path to a wg-access-server config file").Envar("WG_CONFIG").FileVar(&cmd.ConfigFilePath) + cli.Flag("admin-username", "Admin username (defaults to admin)").Envar("WG_ADMIN_USERNAME").Default("admin").StringVar(&cmd.AppConfig.AdminUsername) + cli.Flag("admin-password", "Admin password (provide plaintext, stored in-memory only)").Envar("WG_ADMIN_PASSWORD").Required().StringVar(&cmd.AppConfig.AdminPassword) + cli.Flag("port", "The port that the web ui server will listen on").Envar("WG_PORT").Default("8000").IntVar(&cmd.AppConfig.Port) + 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("storage", "The storage backend connection string").Envar("WG_STORAGE").Default("memory://").StringVar(&cmd.AppConfig.Storage) + cli.Flag("disable-metadata", "Disable metadata collection (i.e. metrics)").Envar("WG_DISABLE_METADATA").Default("false").BoolVar(&cmd.AppConfig.DisableMetadata) + 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("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 } type servecmd struct { - configPath string - webPort int - wireguardPort int - storage string - privateKey string - disableMetadata bool - adminUsername string - adminPassword string - upstreamDNS string + ConfigFilePath *os.File + AppConfig config.AppConfig } func (cmd *servecmd) Name() string { @@ -68,7 +71,7 @@ func (cmd *servecmd) Run() { // WireGuard Server wg := wgembed.NewNoOpInterface() if conf.WireGuard.Enabled { - wgimpl, err := wgembed.New(conf.WireGuard.InterfaceName) + wgimpl, err := wgembed.New(conf.WireGuard.Interface) if err != nil { 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) - 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) } } @@ -160,13 +163,6 @@ func (cmd *servecmd) Run() { // Static website 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 // Listen @@ -184,107 +180,92 @@ func (cmd *servecmd) Run() { } func (cmd *servecmd) ReadConfig() *config.AppConfig { - // here we're filling out the config struct - // with values from our flags/defaults. - config := &config.AppConfig{} - config.Port = cmd.webPort - 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 { + if cmd.ConfigFilePath != nil { + defer cmd.ConfigFilePath.Close() + if b, err := ioutil.ReadAll(cmd.ConfigFilePath); err == nil { + if err := yaml.Unmarshal(b, &cmd.AppConfig); err != nil { logrus.Fatal(errors.Wrap(err, "failed to bind configuration file")) } } } - if config.LogLevel != "" { - level, err := logrus.ParseLevel(config.LogLevel) - if err != nil { - logrus.Fatal(errors.Wrap(err, "invalid log level - should be one of fatal, error, warn, info, debug, trace")) + if cmd.AppConfig.LogLevel != "" { + if level, err := logrus.ParseLevel(cmd.AppConfig.LogLevel); err == nil { + logrus.SetLevel(level) } - 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") } - if config.VPN.GatewayInterface == "" { - iface, err := defaultInterface() - if err != nil { - logrus.Warn(errors.Wrap(err, "failed to set default value for VPN.GatewayInterface")) - } else { - config.VPN.GatewayInterface = iface - } + // set a basic auth entry for the admin user + if cmd.AppConfig.Auth.Basic == nil { + cmd.AppConfig.Auth.Basic = &authconfig.BasicAuthConfig{} + } + pw, err := bcrypt.GenerateFromPassword([]byte(cmd.AppConfig.AdminPassword), bcrypt.DefaultCost) + 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 == "" { - if !strings.HasPrefix(config.Storage, "memory://") { + // we'll generate a private key when using memory:// + // storage only. + if cmd.AppConfig.WireGuard.PrivateKey == "" { + if !strings.HasPrefix(cmd.AppConfig.Storage, "memory://") { logrus.Fatal(missingPrivateKey) } key, err := wgtypes.GeneratePrivateKey() if err != nil { logrus.Fatal(errors.Wrap(err, "failed to generate a server private key")) } - config.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))) + cmd.AppConfig.WireGuard.PrivateKey = key.String() } - return config + return &cmd.AppConfig } func claimsMiddleware(conf *config.AppConfig) authsession.ClaimsMiddleware { return func(user *authsession.Identity) error { - if user.Subject == conf.AdminSubject { + if user.Subject == conf.AdminUsername { user.Claims.Add("admin", "true") } 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() 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 { routes, err := netlink.RouteList(link, 4) 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 { 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: diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..4d4e706 Binary files /dev/null and b/db.sqlite3 differ diff --git a/docker-compose.yml b/docker-compose.yml index 7832676..de31ad7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,9 +13,9 @@ services: - "wg-access-server-data:/data" # - "./config.yaml:/config.yaml" # if you have a custom config file environment: - - "ADMIN_USERNAME=admin" - - "ADMIN_PASSWORD=${ADMIN_PASSWORD:?\n\nplease set the ADMIN_PASSWORD environment variable:\n export 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_ADMIN_USERNAME=admin" + - "WG_ADMIN_PASSWORD=${WG_ADMIN_PASSWORD:?\n\nplease set the WG_ADMIN_PASSWORD environment variable:\n export WG_ADMIN_PASSWORD=example\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: - "8000:8000/tcp" - "51820:51820/udp" diff --git a/docs/2-configuration.md b/docs/2-configuration.md new file mode 100644 index 0000000..16f7b69 --- /dev/null +++ b/docs/2-configuration.md @@ -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: "" +dns: + upstream: + - "8.8.8.8" +``` diff --git a/docs/storage.md b/docs/3-storage.md similarity index 84% rename from docs/storage.md rename to docs/3-storage.md index 567580d..1224199 100644 --- a/docs/storage.md +++ b/docs/3-storage.md @@ -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 | -|----------|------------|-------------|----------------------------------------| -| memory | ❌ | ❌ | Local development | -| sqlite3 | ✔️ | ❌ | Production - single instance deployments | -| postgres | ✔️ | ✔️ (soon) | Production - multi instance deployments | -| mysql | ✔️ | ❌ | Production - single instance deployments | +| Backend | Persistent | Supports HA | Use Case | +| -------- | ---------- | ----------- | ---------------------------------------- | +| memory | ❌ | ❌ | Local development | +| sqlite3 | ✔️ | ❌ | Production - single instance deployments | +| postgres | ✔️ | ✔️ (soon) | Production - multi instance deployments | +| mysql | ✔️ | ❌ | Production - single instance deployments | ## Backends diff --git a/docs/4-auth.md b/docs/4-auth.md new file mode 100644 index 0000000..b0783fd --- /dev/null +++ b/docs/4-auth.md @@ -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 " + 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: "" + clientSecret: "" + # 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: "" + clientSecret: "" + redirectURL: "https:///wg-access-server.example.com/callback" + emailDomains: + - example.com +``` diff --git a/docs/configuration.md b/docs/configuration.md deleted file mode 100644 index 9c75093..0000000 --- a/docs/configuration.md +++ /dev/null @@ -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=""`. - -## 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 " - 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 -``` diff --git a/docs/deployment/2-docker-compose.md b/docs/deployment/2-docker-compose.md index f227c90..3738fa7 100644 --- a/docs/deployment/2-docker-compose.md +++ b/docs/deployment/2-docker-compose.md @@ -3,7 +3,7 @@ You can run wg-access-server using the following example 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. ```yaml diff --git a/go.mod b/go.mod index 1c4a707..ce54ca0 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/docker/libnetwork v0.8.0-dev.2.0.20200217033114-6659f7f4d8c1 github.com/golang/protobuf v1.4.2 github.com/google/uuid v1.1.1 - github.com/gorilla/handlers v1.4.2 github.com/gorilla/mux v1.7.4 github.com/gorilla/sessions v1.2.0 github.com/gorilla/websocket v1.4.2 // indirect diff --git a/go.sum b/go.sum index d453f63..b35c1b6 100644 --- a/go.sum +++ b/go.sum @@ -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/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/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/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/go.mod h1:i09dm8AEkurC4oATFxjvyH0+e1pdmtZoNk2FfPupROI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/config/config.go b/internal/config/config.go index 7d233b4..b8eb909 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,24 +5,41 @@ import ( ) type AppConfig struct { - LogLevel string `yaml:"loglevel"` - DisableMetadata bool `yaml:"disableMetadata"` - AdminSubject string `yaml:"adminSubject"` - AdminPassword string `yaml:"adminPassword"` + // Set the log level. + // Defaults to "info" (fatal, error, warn, info, debug, trace) + LogLevel string `yaml:"loglevel"` + // 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. // Defaults to 8000 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 // be persisted. // Supports memory:// file:// postgres:// mysql:// sqlite3:// // 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 { + // 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"` // The network interface name of the WireGuard // network device. // Defaults to wg0 - InterfaceName string `yaml:"interfaceName"` + Interface string `yaml:"interface"` // The WireGuard PrivateKey // If this value is lost then any existing // clients (WireGuard peers) will no longer @@ -31,15 +48,11 @@ type AppConfig struct { // their connection configuration or setup // their VPN again using the web ui (easier for most people) 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 // Defaults to 51820 Port int `yaml:"port"` } `yaml:"wireguard"` + // Configure VPN related settings (networking) VPN struct { // CIDR configures a network address space // that client (WireGuard peers) will be allocated @@ -56,8 +69,9 @@ type AppConfig struct { // files and in server-side iptable rules // to enforce network access. // 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 { // Enabled allows you to turn on/off // the VPN DNS proxy feature. diff --git a/internal/dnsproxy/server.go b/internal/dnsproxy/server.go index db38707..1fb8818 100644 --- a/internal/dnsproxy/server.go +++ b/internal/dnsproxy/server.go @@ -7,8 +7,6 @@ import ( "strings" "time" - "github.com/docker/libnetwork/resolvconf" - "github.com/docker/libnetwork/types" "github.com/miekg/dns" "github.com/patrickmn/go-cache" "github.com/pkg/errors" @@ -27,23 +25,12 @@ type DNSServer struct { } func New(opts DNSServerOpts) (*DNSServer, error) { - - upstream := opts.Upstream - - 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") + if len(opts.Upstream) == 0 { + return nil, errors.New("at least 1 upstream dns server is required for the dns proxy server to function") } addr := "0.0.0.0:53" - - logrus.Infof("starting dns server on %s with upstreams: %s", addr, strings.Join(upstream, ", ")) + logrus.Infof("starting dns server on %s with upstreams: %s", addr, strings.Join(opts.Upstream, ", ")) dnsServer := &DNSServer{ server: &dns.Server{ @@ -55,7 +42,7 @@ func New(opts DNSServerOpts) (*DNSServer, error) { Timeout: 5 * time.Second, }, cache: cache.New(10*time.Minute, 10*time.Minute), - upstream: upstream, + upstream: opts.Upstream, } dnsServer.server.Handler = dnsServer diff --git a/internal/services/server_service.go b/internal/services/server_service.go index 844235e..5c7c569 100644 --- a/internal/services/server_service.go +++ b/internal/services/server_service.go @@ -33,7 +33,7 @@ func (s *ServerService) Info(ctx context.Context, req *proto.InfoReq) (*proto.In } return &proto.InfoRes{ - Host: stringValue(s.Config.WireGuard.ExternalHost), + Host: stringValue(&s.Config.ExternalHost), PublicKey: publicKey, Port: int32(s.Config.WireGuard.Port), HostVpnIp: network.ServerVPNIP(s.Config.VPN.CIDR).IP.String(), diff --git a/main.go b/main.go index a1f97fa..218e6c4 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "runtime" "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/serve" "github.com/sirupsen/logrus" @@ -15,20 +16,24 @@ import ( var ( 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() - - servecmd = serve.RegisterCommand(app) - migratecmd = migrate.RegisterCommand(app) + logLevel = app.Flag("log-level", "Log level: trace, debug, info, error, fatal").Envar("LOG_LEVEL").Default("info").String() ) 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) if err != nil { logrus.Fatal(errors.Wrap(err, "invalid log level - should be one of fatal, error, warn, info, debug, trace")) } - logrus.SetLevel(level) logrus.SetReportCaller(true) logrus.SetFormatter(&logrus.TextFormatter{ @@ -37,12 +42,10 @@ func main() { }, }) - switch cmd { - case servecmd.Name(): - servecmd.Run() - case migratecmd.Name(): - migratecmd.Run() - default: - logrus.Fatal(fmt.Errorf("unknown command: %s", cmd)) + for _, c := range commands { + if clicmd == c.Name() { + c.Run() + return + } } }