Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c302b1d719 | ||
|
|
ee31dff01e |
+5
-13
@@ -12,25 +12,17 @@ RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}"
|
|||||||
|
|
||||||
FROM alpine:3.21
|
FROM alpine:3.21
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates tzdata \
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
&& addgroup -S metacrypt \
|
|
||||||
&& adduser -S -G metacrypt -h /srv/metacrypt -s /sbin/nologin metacrypt \
|
|
||||||
&& mkdir -p /srv/metacrypt && chown metacrypt:metacrypt /srv/metacrypt
|
|
||||||
|
|
||||||
COPY --from=builder /metacrypt /usr/local/bin/metacrypt
|
COPY --from=builder /metacrypt /usr/local/bin/metacrypt
|
||||||
|
|
||||||
# /srv/metacrypt is the single volume mount point.
|
# /srv/metacrypt (config, certs/, metacrypt.db) is bind-mounted at runtime by
|
||||||
# It must contain:
|
# MCP. No VOLUME / pre-created dir / USER — the agent supplies --user 0:0, so
|
||||||
# metacrypt.toml — configuration file
|
# the image unpacks and runs cleanly under rootless podman.
|
||||||
# certs/ — TLS certificate and key
|
WORKDIR /
|
||||||
# metacrypt.db — created automatically on first run
|
|
||||||
VOLUME /srv/metacrypt
|
|
||||||
WORKDIR /srv/metacrypt
|
|
||||||
|
|
||||||
EXPOSE 8443
|
EXPOSE 8443
|
||||||
EXPOSE 9443
|
EXPOSE 9443
|
||||||
|
|
||||||
USER metacrypt
|
|
||||||
|
|
||||||
ENTRYPOINT ["metacrypt"]
|
ENTRYPOINT ["metacrypt"]
|
||||||
CMD ["server", "--config", "/srv/metacrypt/metacrypt.toml"]
|
CMD ["server", "--config", "/srv/metacrypt/metacrypt.toml"]
|
||||||
|
|||||||
+6
-12
@@ -12,23 +12,17 @@ RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}"
|
|||||||
|
|
||||||
FROM alpine:3.21
|
FROM alpine:3.21
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates tzdata \
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
&& addgroup -S metacrypt \
|
|
||||||
&& adduser -S -G metacrypt -h /srv/metacrypt -s /sbin/nologin metacrypt \
|
|
||||||
&& mkdir -p /srv/metacrypt && chown metacrypt:metacrypt /srv/metacrypt
|
|
||||||
|
|
||||||
COPY --from=builder /metacrypt-web /usr/local/bin/metacrypt-web
|
COPY --from=builder /metacrypt-web /usr/local/bin/metacrypt-web
|
||||||
|
|
||||||
# /srv/metacrypt is the single volume mount point.
|
# /srv/metacrypt is bind-mounted at runtime by MCP (config + certs/). The
|
||||||
# It must contain:
|
# image deliberately does NOT declare a VOLUME or pre-create/chown the
|
||||||
# metacrypt.toml — configuration file
|
# directory, and does NOT set USER (the agent supplies --user 0:0), so it
|
||||||
# certs/ — TLS certificate and key
|
# unpacks and runs cleanly under rootless podman.
|
||||||
VOLUME /srv/metacrypt
|
WORKDIR /
|
||||||
WORKDIR /srv/metacrypt
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
USER metacrypt
|
|
||||||
|
|
||||||
ENTRYPOINT ["metacrypt-web"]
|
ENTRYPOINT ["metacrypt-web"]
|
||||||
CMD ["--config", "/srv/metacrypt/metacrypt.toml"]
|
CMD ["--config", "/srv/metacrypt/metacrypt.toml"]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ go 1.25.7
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
git.wntrmute.dev/kyle/goutils v1.21.0
|
git.wntrmute.dev/kyle/goutils v1.21.0
|
||||||
git.wntrmute.dev/mc/mcdsl v1.7.0
|
git.wntrmute.dev/mc/mcdsl v1.9.0
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
git.wntrmute.dev/kyle/goutils v1.21.0 h1:ZR7ovV400hsF09zc8tkdHs6vyen8TDJ7flong/dnFXM=
|
git.wntrmute.dev/kyle/goutils v1.21.0 h1:ZR7ovV400hsF09zc8tkdHs6vyen8TDJ7flong/dnFXM=
|
||||||
git.wntrmute.dev/kyle/goutils v1.21.0/go.mod h1:JQ8NL5lHSEYl719UMf20p4G1ei70RVGma0hjjNXCR2c=
|
git.wntrmute.dev/kyle/goutils v1.21.0/go.mod h1:JQ8NL5lHSEYl719UMf20p4G1ei70RVGma0hjjNXCR2c=
|
||||||
git.wntrmute.dev/mc/mcdsl v1.7.0 h1:dAh2SGdzjhz0H66i3KAMDm1eRYYgMaxqQ0Pj5NzF7fc=
|
git.wntrmute.dev/mc/mcdsl v1.9.0 h1:TGqVhf9uhhh5jpMhN+8eNtBPSi/wwNXQn/NFDAcU4wg=
|
||||||
git.wntrmute.dev/mc/mcdsl v1.7.0/go.mod h1:MhYahIu7Sg53lE2zpQ20nlrsoNRjQzOJBAlCmom2wJc=
|
git.wntrmute.dev/mc/mcdsl v1.9.0/go.mod h1:MhYahIu7Sg53lE2zpQ20nlrsoNRjQzOJBAlCmom2wJc=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
|||||||
@@ -37,8 +37,16 @@ type MCIASConfig struct {
|
|||||||
// SSOConfig holds SSO redirect settings for the web UI.
|
// SSOConfig holds SSO redirect settings for the web UI.
|
||||||
type SSOConfig struct {
|
type SSOConfig struct {
|
||||||
// RedirectURI is the callback URL that MCIAS redirects to after login.
|
// RedirectURI is the callback URL that MCIAS redirects to after login.
|
||||||
// Must exactly match the redirect_uri registered in MCIAS config.
|
// Must exactly match the redirect_uri registered in MCIAS config. For
|
||||||
|
// public (non-Tailnet) browser access this must be the public hostname.
|
||||||
RedirectURI string `toml:"redirect_uri"`
|
RedirectURI string `toml:"redirect_uri"`
|
||||||
|
|
||||||
|
// PublicURL is the browser-facing MCIAS base URL used to build the SSO
|
||||||
|
// authorize redirect (e.g. "https://mcias.metacircular.net"). When empty,
|
||||||
|
// the backend [mcias].server_url is used for the redirect too. Set this
|
||||||
|
// when browsers cannot resolve the internal MCIAS name; the
|
||||||
|
// server-to-server code exchange still uses [mcias].server_url.
|
||||||
|
PublicURL string `toml:"public_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebConfig holds settings for the standalone web UI server (metacrypt-web).
|
// WebConfig holds settings for the standalone web UI server (metacrypt-web).
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ func New(cfg *config.Config, logger *slog.Logger) (*WebServer, error) {
|
|||||||
if cfg.SSO.RedirectURI != "" {
|
if cfg.SSO.RedirectURI != "" {
|
||||||
ssoClient, ssoErr := mcdsso.New(mcdsso.Config{
|
ssoClient, ssoErr := mcdsso.New(mcdsso.Config{
|
||||||
MciasURL: cfg.MCIAS.ServerURL,
|
MciasURL: cfg.MCIAS.ServerURL,
|
||||||
|
PublicURL: cfg.SSO.PublicURL,
|
||||||
ClientID: "metacrypt",
|
ClientID: "metacrypt",
|
||||||
RedirectURI: cfg.SSO.RedirectURI,
|
RedirectURI: cfg.SSO.RedirectURI,
|
||||||
CACert: cfg.MCIAS.CACert,
|
CACert: cfg.MCIAS.CACert,
|
||||||
@@ -183,7 +184,12 @@ func New(cfg *config.Config, logger *slog.Logger) (*WebServer, error) {
|
|||||||
return nil, fmt.Errorf("webserver: create SSO client: %w", ssoErr)
|
return nil, fmt.Errorf("webserver: create SSO client: %w", ssoErr)
|
||||||
}
|
}
|
||||||
ws.ssoClient = ssoClient
|
ws.ssoClient = ssoClient
|
||||||
logger.Info("SSO enabled: redirecting to MCIAS for login", "mcias_url", cfg.MCIAS.ServerURL)
|
authorizeURL := cfg.SSO.PublicURL
|
||||||
|
if authorizeURL == "" {
|
||||||
|
authorizeURL = cfg.MCIAS.ServerURL
|
||||||
|
}
|
||||||
|
logger.Info("SSO enabled: redirecting to MCIAS for login",
|
||||||
|
"authorize_url", authorizeURL, "exchange_url", cfg.MCIAS.ServerURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tok := cfg.MCIAS.ServiceToken; tok != "" {
|
if tok := cfg.MCIAS.ServiceToken; tok != "" {
|
||||||
|
|||||||
+6
-8
@@ -65,11 +65,11 @@ func Open(path string) (*sql.DB, error) {
|
|||||||
// connection to serialize all access and eliminate busy errors.
|
// connection to serialize all access and eliminate busy errors.
|
||||||
database.SetMaxOpenConns(1)
|
database.SetMaxOpenConns(1)
|
||||||
|
|
||||||
// Ensure permissions are correct even if the file already existed.
|
// Best-effort permissions tightening. This may fail inside rootless
|
||||||
if err := os.Chmod(path, 0600); err != nil {
|
// podman containers where fchmod is denied in the user namespace.
|
||||||
_ = database.Close()
|
// The database still functions correctly without it.
|
||||||
return nil, fmt.Errorf("db: chmod %s: %w", path, err)
|
// See: log/2026-04-03-uid-incident.md
|
||||||
}
|
_ = os.Chmod(path, 0600)
|
||||||
|
|
||||||
return database, nil
|
return database, nil
|
||||||
}
|
}
|
||||||
@@ -168,9 +168,7 @@ func Snapshot(database *sql.DB, destPath string) error {
|
|||||||
return fmt.Errorf("db: snapshot: %w", err)
|
return fmt.Errorf("db: snapshot: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Chmod(destPath, 0600); err != nil {
|
_ = os.Chmod(destPath, 0600) // best-effort; may fail in rootless containers
|
||||||
return fmt.Errorf("db: chmod snapshot %s: %w", destPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-3
@@ -39,9 +39,18 @@ const (
|
|||||||
// Config holds the SSO client configuration. The values must match the
|
// Config holds the SSO client configuration. The values must match the
|
||||||
// SSO client registration in MCIAS config.
|
// SSO client registration in MCIAS config.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// MciasURL is the base URL of the MCIAS server.
|
// MciasURL is the base URL of the MCIAS server used for the
|
||||||
|
// server-to-server authorization-code exchange. This is typically the
|
||||||
|
// internal/Tailnet address so the exchange does not depend on the public
|
||||||
|
// edge.
|
||||||
MciasURL string
|
MciasURL string
|
||||||
|
|
||||||
|
// PublicURL, when set, is the browser-facing MCIAS base URL used to build
|
||||||
|
// the authorize redirect. It must be resolvable and reachable by end-user
|
||||||
|
// browsers (e.g. the public hostname). When empty, MciasURL is used for
|
||||||
|
// both, which only works when MciasURL is itself browser-reachable.
|
||||||
|
PublicURL string
|
||||||
|
|
||||||
// ClientID is the registered SSO client identifier.
|
// ClientID is the registered SSO client identifier.
|
||||||
ClientID string
|
ClientID string
|
||||||
|
|
||||||
@@ -104,9 +113,19 @@ func New(cfg Config) (*Client, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeURL returns the MCIAS authorize URL with the given state parameter.
|
// authorizeBase returns the browser-facing MCIAS base URL: PublicURL when
|
||||||
|
// configured, otherwise MciasURL.
|
||||||
|
func (c *Client) authorizeBase() string {
|
||||||
|
if c.cfg.PublicURL != "" {
|
||||||
|
return c.cfg.PublicURL
|
||||||
|
}
|
||||||
|
return c.cfg.MciasURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeURL returns the MCIAS authorize URL (browser-facing) with the
|
||||||
|
// given state parameter.
|
||||||
func (c *Client) AuthorizeURL(state string) string {
|
func (c *Client) AuthorizeURL(state string) string {
|
||||||
base := strings.TrimRight(c.cfg.MciasURL, "/")
|
base := strings.TrimRight(c.authorizeBase(), "/")
|
||||||
return base + "/sso/authorize?" + url.Values{
|
return base + "/sso/authorize?" + url.Values{
|
||||||
"client_id": {c.cfg.ClientID},
|
"client_id": {c.cfg.ClientID},
|
||||||
"redirect_uri": {c.cfg.RedirectURI},
|
"redirect_uri": {c.cfg.RedirectURI},
|
||||||
|
|||||||
Vendored
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
## explicit; go 1.24.0
|
## explicit; go 1.24.0
|
||||||
git.wntrmute.dev/kyle/goutils/certlib/certgen
|
git.wntrmute.dev/kyle/goutils/certlib/certgen
|
||||||
git.wntrmute.dev/kyle/goutils/lib
|
git.wntrmute.dev/kyle/goutils/lib
|
||||||
# git.wntrmute.dev/mc/mcdsl v1.7.0
|
# git.wntrmute.dev/mc/mcdsl v1.9.0
|
||||||
## explicit; go 1.25.7
|
## explicit; go 1.25.7
|
||||||
git.wntrmute.dev/mc/mcdsl/auth
|
git.wntrmute.dev/mc/mcdsl/auth
|
||||||
git.wntrmute.dev/mc/mcdsl/config
|
git.wntrmute.dev/mc/mcdsl/config
|
||||||
|
|||||||
Reference in New Issue
Block a user