This commit is contained in:
Stavros
2026-01-31 10:33:55 +02:00
parent 252ba10f48
commit 0cd3ba8236
10 changed files with 461 additions and 95 deletions

View File

@@ -45,6 +45,7 @@ func (app *BootstrapApp) Setup() error {
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
return fmt.Errorf("session max lifetime cannot be less than session expiry")
}
// Parse users
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
@@ -203,6 +204,19 @@ func (app *BootstrapApp) Setup() error {
return nil
}
// If we have tailscale configured, we will listen on it
if app.services.tailscaleService.IsConfigured() {
ln, err := app.services.tailscaleService.CreateListener()
if err != nil {
return fmt.Errorf("failed to create listener: %w", err)
}
if err := router.RunListener(ln); err != nil {
tlog.App.Fatal().Err(err).Msg("Failed to start tailscale listener")
}
}
// Start server
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
tlog.App.Info().Msgf("Starting server on %s", address)

View File

@@ -12,6 +12,7 @@ type Services struct {
dockerService *service.DockerService
ldapService *service.LdapService
oauthBrokerService *service.OAuthBrokerService
tailscaleService *service.TailscaleService
}
func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, error) {
@@ -36,6 +37,19 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
tlog.App.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without it")
}
tailscaleService := service.NewTailscaleService(service.TailscaleServiceConfig{
AuthKey: app.config.Tailscale.AuthKey,
Hostname: app.config.Tailscale.Hostname,
})
err = tailscaleService.Init()
if err != nil {
return Services{}, err
}
services.tailscaleService = tailscaleService
dockerService := service.NewDockerService()
err = dockerService.Init()
@@ -68,7 +82,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
SessionCookieName: app.context.sessionCookieName,
IP: app.config.Auth.IP,
LDAPGroupsCacheTTL: app.config.Ldap.GroupCacheTTL,
}, dockerService, services.ldapService, queries)
}, dockerService, services.ldapService, services.tailscaleService, queries)
err = authService.Init()

View File

@@ -29,6 +29,7 @@ type Config struct {
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
Log LogConfig `description:"Logging configuration." yaml:"log"`
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"`
}
type ServerConfig struct {
@@ -84,6 +85,11 @@ type LogConfig struct {
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams"`
}
type TailscaleConfig struct {
AuthKey string `description:"Tailscale authentication key." yaml:"authKey"`
Hostname string `description:"Hostname for Tailscale." yaml:"hostname"`
}
type LogStreams struct {
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http"`
App LogStreamConfig `description:"Application logging." yaml:"app"`

View File

@@ -94,6 +94,17 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
proto := c.Request.Header.Get("X-Forwarded-Proto")
host := c.Request.Header.Get("X-Forwarded-Host")
// Is it from tailscale?
tailscaleWho, err := controller.auth.IsTailscale(c)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to get tailscale whois")
controller.handleError(c, req, isBrowser)
return
}
tlog.App.Debug().Interface("who", tailscaleWho).Msg("Yoyo are we from tailscale")
// Get acls
acls, err := controller.acls.GetAccessControls(host)

View File

@@ -37,6 +37,25 @@ func (m *ContextMiddleware) Init() error {
func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Before checking the cookie, let's check tailscale
who, err := m.auth.IsTailscale(c)
if err != nil {
tlog.App.Warn().Err(err).Msg("Failed to check Tailscale, checking other auth sources")
}
if who != nil {
c.Set("context", &config.UserContext{
Username: who.Node.Name,
Name: who.Node.ComputedName,
Email: strings.Replace(who.Node.Name, ".", "@", 1),
Provider: "tailscale",
IsLoggedIn: true,
})
c.Next()
return
}
cookie, err := m.auth.GetSessionCookie(c)
if err != nil {

View File

@@ -13,6 +13,8 @@ import (
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
tsLocal "tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -52,16 +54,18 @@ type AuthService struct {
loginMutex sync.RWMutex
ldapGroupsMutex sync.RWMutex
ldap *LdapService
tailscale *TailscaleService
queries *repository.Queries
}
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries) *AuthService {
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, tailscale *TailscaleService, queries *repository.Queries) *AuthService {
return &AuthService{
config: config,
docker: docker,
loginAttempts: make(map[string]*LoginAttempt),
ldapGroupsCache: make(map[string]*LdapGroupsCache),
ldap: ldap,
tailscale: tailscale,
queries: queries,
}
}
@@ -553,3 +557,20 @@ func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
tlog.App.Debug().Str("ip", ip).Msg("IP not in bypass list, continuing with authentication")
return false
}
func (auth *AuthService) IsTailscale(c *gin.Context) (*apitype.WhoIsResponse, error) {
if !auth.tailscale.IsConfigured() {
return nil, nil
}
who, err := auth.tailscale.Whois(c, c.Request.RemoteAddr)
if err != nil {
if errors.Is(err, tsLocal.ErrPeerNotFound) {
return nil, nil
}
return nil, err
}
return who, nil
}

View File

@@ -0,0 +1,83 @@
package service
import (
"context"
"errors"
"net"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/tsnet"
)
var ErrNotConfigured = errors.New("tailscale service is not configured")
type TailscaleServiceConfig struct {
AuthKey string
Hostname string
}
type TailscaleService struct {
config TailscaleServiceConfig
server *tsnet.Server
client *local.Client
}
func NewTailscaleService(config TailscaleServiceConfig) *TailscaleService {
return &TailscaleService{
config: config,
}
}
func (service *TailscaleService) Init() error {
if !service.IsConfigured() {
return nil
}
server := new(tsnet.Server)
if service.config.AuthKey == "" {
tlog.App.Info().Msg("Auth key is empty but Tailscale is configured, a login link will appear below so as you can authenticate")
}
server.Hostname = service.config.Hostname
server.AuthKey = service.config.AuthKey
client, err := server.LocalClient()
if err != nil {
return err
}
service.client = client
service.server = server
return nil
}
func (service *TailscaleService) IsConfigured() bool {
return service.config.Hostname != ""
}
func (service *TailscaleService) Whois(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
if !service.IsConfigured() {
return nil, ErrNotConfigured
}
return service.client.WhoIs(ctx, remoteAddr)
}
func (service *TailscaleService) CreateListener() (net.Listener, error) {
if !service.IsConfigured() {
return nil, ErrNotConfigured
}
ln, err := service.server.ListenTLS("tcp", ":443")
if err != nil {
return nil, err
}
return ln, nil
}