git-cone is a security-hardened hard fork of
soft-serve. It is intended to
stay as drop-in compatible as practical while focusing on security fixes and
operational hardening, not on growing the core product surface.
cone is the primary CLI. soft remains available as a compatibility layer.
This fork includes security work imported and adapted from
dvrd's soft-serve branch, including the patch set
described in local commit 8eb4b04 ("apply dvrd rounds 46-59 fixes"). That
import covered fixes such as SSRF/JWT hardening and several backend/store
corrections.
Additional hardening was then implemented directly in git-cone:
- SSH was hardened using stricter KEX, cipher, and MAC defaults in pkg/ssh/ssh.go.
- SSH stdin was hardened using input rate limiting in pkg/ssh/middleware.go.
- File serving was hardened by removing the
sendFileTOCTOU window in pkg/web/git.go. - User deletion was hardened by fixing repo and row deletion ordering in pkg/backend/user.go.
As of v0.13.X the internals between soft-serve and git-cone have diverged
a bit more, please don't take this list as exhaustive.
- Pure Go build, including SQLite via
modernc.org/sqlite - SSH TUI and SSH command interface
- HTTP Git smart protocol and LFS
- Dual env-prefix support:
GIT_CONE_*overridesSOFT_SERVE_* - Optional Gotify notifications for security-relevant events
- Strict mode for hardened deployments
git://disabled by defaultcone audit,repo verify, and/health
Build locally:
make build
./dist/cone serveOr enter the Guix development shell first:
make shell
make build
make testFirst-time SSH admin flow:
- Start the server with
GIT_CONE_INITIAL_ADMIN_KEYSpointing to your public key. - Connect with
ssh -p 23231 git@hostfor the TUI. - Run admin commands over SSH, for example:
ssh -p 23231 host user create alicessh -p 23231 host repo create demossh -p 23231 host audit
Important
The latest tag is literally the latest image built wheter it was tagged
not, this includes development builds. Use a pinned version.
docker pull ghcr.io/urutau-ltd/git-cone:<tag>Minimal Compose example:
services:
git-cone:
image: ghcr.io/urutau-ltd/git-cone:latest
ports:
- "23231:23231"
- "127.0.0.1:23232:23232"
volumes:
- git-cone-data:/git-cone/data
- ./git-cone/hooks:/git-cone/data/hooks
environment:
- GIT_CONE_DATA_PATH=/git-cone/data
- GIT_CONE_INITIAL_ADMIN_KEYS=ssh-ed25519 AAAA...
- GIT_CONE_SSH_PUBLIC_URL=ssh://git.example.com
- GIT_CONE_HTTP_PUBLIC_URL=https://git.example.com
- GIT_CONE_NAME=Git Cone
- GIT_CONE_SECURITY_STRICT=true
restart: unless-stopped
volumes:
git-cone-data:Container notes:
- data lives at
/git-cone/data - hooks live at
/git-cone/data/hooks - the image provides both
coneandsoft /healthis intended for local container health checks such as Docker/Dozzle
This fork aims to remain a practical drop-in replacement for recent soft-serve
deployments.
| Before | After |
|---|---|
soft serve |
cone serve or soft serve |
soft browse |
cone browse or soft browse |
SOFT_SERVE_* |
GIT_CONE_* preferred, SOFT_SERVE_* still supported |
What changed on purpose:
- the preferred binary name is now
cone softstill works and maps to the same implementation- the default server name is
Git Cone - the image stores data in
/git-cone/data git://is off by default- the server is SQLite-only
This fork intentionally diverges from upstream in a few places. These are the ones operators usually need to know before a migration:
| Area | soft-serve expectation |
git-cone behavior |
|---|---|---|
| Primary binary | soft |
cone is preferred; soft remains as compatibility wrapper |
| Environment prefix | SOFT_SERVE_* |
GIT_CONE_* is preferred; SOFT_SERVE_* still works |
| Data path in container | /soft-serve |
/git-cone/data |
| Default server name | Upstream default | Git Cone |
| Database backends | Upstream had more room for alternate drivers | SQLite-only |
git:// daemon |
Historically available by default | Disabled by default |
| Strict mode | Not present | Available via security.strict |
| SSH crypto defaults | Upstream defaults | Hardened KEX/cipher/MAC policy, including post-quantum KEX for newer OpenSSH clients |
| Health endpoint | Not present | GET /health returns JSON |
| Audit command | Not present | ssh host audit |
| Repo integrity check | Not present | ssh host repo verify <repo> |
| Notifications | No upstream support | Optional Gotify notifications added by this fork |
Behavior that stays intentionally compatible:
- SSH TUI remains the main interface
- Git over SSH and HTTP still work the same way
soft serve,soft browse, andSOFT_SERVE_*still work- the SSH command surface stays close to upstream, with additive hardening features
For existing Compose stacks, the least disruptive migration is:
- keep the service name as
soft-serve - keep the volume name as
soft-serve-data - switch the image to
ghcr.io/urutau-ltd/git-cone:latest - mount that volume at
/git-cone/data - set
GIT_CONE_DATA_PATH=/git-cone/data
Example drop-in replacement:
services:
soft-serve:
image: ghcr.io/urutau-ltd/git-cone:latest
ports:
- "23231:23231"
- "23232:23232"
volumes:
- soft-serve-data:/git-cone/data
- ./soft-serve/hooks:/git-cone/data/hooks
environment:
- GIT_CONE_DATA_PATH=/git-cone/data
- GIT_CONE_INITIAL_ADMIN_KEYS=${SOFT_SERVE_ADMIN_KEY}
- GIT_CONE_SSH_PUBLIC_URL=ssh://git.example.com
- GIT_CONE_HTTP_PUBLIC_URL=https://git.example.com
- GIT_CONE_NAME=Git Cone
- GIT_CONE_SECURITY_STRICT=true
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:23232/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15sThis section documents the actual command surface exposed by the current fork. It covers:
- the local binary CLI:
coneandsoft - the SSH command interface users run against the server
- the repo subcommands under
repo - the internal transport commands Git uses under SSH
cone is the primary binary. soft is a compatibility wrapper over the same
implementation.
| Command | Purpose | Notes |
|---|---|---|
cone serve |
Start the server | soft serve works too |
cone browse [PATH] |
Open the local TUI against a repository on disk | Defaults to . |
cone admin migrate |
Migrate the SQLite schema to the latest version | Local admin command |
cone admin rollback |
Roll back the previous database migration | Local admin command |
cone admin sync-hooks |
Rewrite server-managed hooks for all repos | Local admin command |
cone hook pre-receive |
Internal hook entrypoint | Hidden; called by Git |
cone hook update |
Internal hook entrypoint | Hidden; called by Git |
cone hook post-receive |
Internal hook entrypoint | Hidden; called by Git |
cone hook post-update |
Internal hook entrypoint | Hidden; called by Git |
cone man |
Generate a manpage | Hidden |
cone serve
--sync-hooksRewrite hooks for all repositories before the server starts.
cone browse [PATH]
- no extra flags
cone admin migrate
- no extra flags
cone admin rollback
- no extra flags
cone admin sync-hooks
- no extra flags
cone hook ...
--configDeprecated and ignored. Hook execution reads from the loaded config and environment.
The SSH interface has three modes:
ssh -p 23231 hostOpens the interactive TUI.ssh -p 23231 host <repo>Opens the TUI directly on a readable repository.ssh -p 23231 host <command> ...Runs a non-interactive SSH command.
The SSH command help is dynamic and uses the configured public SSH URL to print the correct host and port in examples.
These are the public non-interactive commands available over SSH.
| Command | Purpose | Access |
|---|---|---|
audit |
Show server/session audit information | Any authenticated user; limited output for unauthenticated sessions |
doctor |
Show effective server security and runtime settings | Admin |
info |
Show information about the current user | Authenticated user |
jwt [repository1 repository2...] |
Mint a JWT scoped to the given audience/repositories | Authenticated user |
pubkey ... |
Manage your own SSH public keys | Authenticated user |
repo ... |
Manage repositories and inspect repo content | Varies by subcommand |
set-username USERNAME |
Change your own username | Authenticated user |
settings ... |
Read or change server-wide access settings | Admin |
token ... |
Manage your own access tokens | Authenticated user |
user ... |
Manage users | Admin |
ssh -p 23231 host audit
Prints:
- server version
- current username and admin state when available
- remote client address and SSH client version
- active public key fingerprint
- active public key algorithm
- public key age when the DB has
created_at - auth mode and whether the session is keyless
- negotiated hostkey, cipher, KEX, and whether the KEX is post-quantum when the session exposes them
- owned and collaborator repo counts
Unauthenticated or keyless sessions only get the server version line.
ssh -p 23231 host doctor
Prints the effective server-side settings that matter for operations and hardening, including:
- strict mode state
- effective SSH/HTTP/stats/git listen addresses and public URLs
- LFS and LFS-over-SSH state
- hook timeout and SSH timeouts
- host/client key paths and whether those files exist
- hardened SSH KEX, cipher, and MAC policy
ssh -p 23231 host info
Prints the current user's account details.
ssh -p 23231 host jwt
ssh -p 23231 host jwt repo1 repo2
Creates a signed JWT using the server JWK pair. The listed repositories become the JWT audience.
ssh -p 23231 host set-username new-name
Changes the username of the authenticated user.
Manage the public keys attached to the current account.
| Command | Purpose |
|---|---|
pubkey add AUTHORIZED_KEY |
Add a public key to your account |
pubkey remove AUTHORIZED_KEY |
Remove a public key from your account |
pubkey list |
List your current public keys |
Aliases:
pubkeyspublickeypublickeys
Examples:
ssh -p 23231 host pubkey list
ssh -p 23231 host pubkey add "ssh-ed25519 AAAA..."
ssh -p 23231 host pubkey remove "ssh-ed25519 AAAA..."Manage HTTP/API access tokens for the current user.
| Command | Purpose |
|---|---|
token create NAME |
Create a token and print it once |
token list |
List token IDs, names, creation dates, and expiration state |
token delete ID |
Delete a token by numeric ID |
Aliases:
token: alias groupaccess-tokentoken list: aliaslstoken delete: aliasesrm,remove
Flags:
token create NAME
--expires-inExpiration duration such as1y,3mo,2w,5d4h, or1h30m.
Examples:
ssh -p 23231 host token create "ci bot"
ssh -p 23231 host token create "pipe clone token" --expires-in 90d
ssh -p 23231 host token list
ssh -p 23231 host token delete 3These are server-wide settings stored in the database. They are admin-only.
| Command | Purpose |
|---|---|
| `settings allow-keyless [true | false]` |
settings anon-access [ACCESS_LEVEL] |
Get or set anonymous access level |
Valid ACCESS_LEVEL values:
no-accessread-onlyread-writeadmin-access
Examples:
ssh -p 23231 host settings allow-keyless false
ssh -p 23231 host settings anon-access no-accessNote:
- when
security.strictis enabled,allow-keylessis forced off - when
security.strictis enabled,anon-accessis forced tono-access
All user commands are admin-only.
| Command | Purpose |
|---|---|
user create USERNAME |
Create a new user |
user delete USERNAME |
Delete a user |
user list |
List all users |
user add-pubkey USERNAME AUTHORIZED_KEY |
Add a key to a user |
user remove-pubkey USERNAME AUTHORIZED_KEY |
Remove a key from a user |
| `user set-admin USERNAME [true | false]` |
user info USERNAME |
Show user details |
user set-username USERNAME NEW_USERNAME |
Rename a user |
Aliases:
user: alias groupusersuser list: aliasls
Flags:
user create USERNAME
-a,--adminCreate the user as admin.-k,--keyAttach an initial public key.
Examples:
ssh -p 23231 host user create alice
ssh -p 23231 host user create pipe-bot --key "ssh-ed25519 AAAA..."
ssh -p 23231 host user create release-bot --admin
ssh -p 23231 host user set-admin alice true
ssh -p 23231 host user info alicerepo is the largest command group.
Aliases for the group:
reposrepositoryrepositories
The subcommands below are available under:
ssh -p 23231 host repo ...
| Command | Purpose | Access |
|---|---|---|
repo list |
List readable repositories | Readable |
repo info REPOSITORY |
Print repo metadata, branches, and tags | Readable |
repo description REPOSITORY [DESCRIPTION] |
Get or set description | Write/admin |
repo project-name REPOSITORY [NAME] |
Get or set project name | Write/admin |
| `repo private REPOSITORY [true | false]` | Get or set private flag |
| `repo hidden REPOSITORY [true | false]` | Get or set hidden flag |
repo is-mirror REPOSITORY |
Report whether the repo is a mirror | Readable |
Flags:
repo list
-a,--allInclude hidden repositories that are otherwise readable.
Examples:
ssh -p 23231 host repo list
ssh -p 23231 host repo list --all
ssh -p 23231 host repo info demo
ssh -p 23231 host repo description demo "Internal deployment repo"
ssh -p 23231 host repo private demo true| Command | Purpose | Access |
|---|---|---|
repo create REPOSITORY |
Create a repository | Write/admin |
repo import REPOSITORY REMOTE |
Import a repository from a remote URL | Write/admin |
repo delete REPOSITORY |
Delete a repository | Admin |
repo rename REPOSITORY NEW_NAME |
Rename a repository | Admin |
repo verify REPOSITORY |
Run git fsck --full |
Write/admin |
Flags:
repo create REPOSITORY
-p,--private-d,--description-n,--name-H,--hidden
repo import REPOSITORY REMOTE
--lfs--lfs-endpoint-m,--mirror-p,--private-d,--description-n,--name-H,--hidden
Examples:
ssh -p 23231 host repo create demo
ssh -p 23231 host repo create secret-repo --private --description "confidential"
ssh -p 23231 host repo import upstream https://example.com/repo.git --mirror
ssh -p 23231 host repo verify demo
ssh -p 23231 host repo rename demo demo-archive| Command | Purpose | Access |
|---|---|---|
repo blob REPOSITORY [REFERENCE] [PATH] |
Print a file from a commit/tree | Readable |
repo tree REPOSITORY [REFERENCE] [PATH] |
Print the repository tree | Readable |
repo commit REPOSITORY SHA |
Print a commit diff | Readable |
Flags:
repo blob REPOSITORY [REFERENCE] [PATH]
-r,--raw-l,--linenumber-c,--color
repo commit REPOSITORY SHA
-c,--color-p,--patch
Examples:
ssh -p 23231 host repo blob demo HEAD README.md
ssh -p 23231 host repo blob demo HEAD README.md --raw
ssh -p 23231 host repo tree demo HEAD
ssh -p 23231 host repo commit demo 0123456789abcdef --patch| Command | Purpose | Access |
|---|---|---|
repo branch list REPOSITORY |
List branches | Readable |
repo branch default REPOSITORY [BRANCH] |
Get or set the default branch | Write/admin |
repo branch delete REPOSITORY BRANCH |
Delete a branch | Write/admin |
Examples:
ssh -p 23231 host repo branch list demo
ssh -p 23231 host repo branch default demo main
ssh -p 23231 host repo branch delete demo old-branch| Command | Purpose | Access |
|---|---|---|
repo tag list REPOSITORY |
List tags | Readable |
repo tag delete REPOSITORY TAG |
Delete a tag | Write/admin |
Examples:
ssh -p 23231 host repo tag list demo
ssh -p 23231 host repo tag delete demo v0.1.0| Command | Purpose | Access |
|---|---|---|
repo collab add REPOSITORY USERNAME [LEVEL] |
Add a collaborator | Admin |
repo collab remove REPOSITORY USERNAME |
Remove a collaborator | Admin |
repo collab list REPOSITORY |
List collaborators | Admin |
Valid collaborator levels:
no-accessread-onlyread-writeadmin-access
If omitted, repo collab add defaults to read-write.
Examples:
ssh -p 23231 host repo collab add demo alice read-only
ssh -p 23231 host repo collab add demo bob admin-access
ssh -p 23231 host repo collab list demo
ssh -p 23231 host repo collab remove demo aliceAll webhook commands are admin-only for the target repository.
| Command | Purpose |
|---|---|
repo webhook list REPOSITORY |
List webhooks |
repo webhook create REPOSITORY URL |
Create a webhook |
repo webhook delete REPOSITORY WEBHOOK_ID |
Delete a webhook |
repo webhook update REPOSITORY WEBHOOK_ID |
Update a webhook |
repo webhook deliveries list REPOSITORY WEBHOOK_ID |
List delivery attempts |
repo webhook deliveries redeliver REPOSITORY WEBHOOK_ID DELIVERY_ID |
Redeliver one attempt |
repo webhook deliveries get REPOSITORY WEBHOOK_ID DELIVERY_ID |
Inspect one delivery |
Aliases:
repo webhook: aliaswebhooksrepo webhook deliveries: aliasesdelivery,deliver
Flags:
repo webhook create REPOSITORY URL
-e,--events-s,--secret-a,--active-c,--content-type
repo webhook update REPOSITORY WEBHOOK_ID
-e,--events-s,--secret-a,--active-c,--content-type-u,--url
--content-type accepts:
jsonform
Examples:
ssh -p 23231 host repo webhook list demo
ssh -p 23231 host repo webhook create demo https://example.com/hook --events push --secret supersecret
ssh -p 23231 host repo webhook update demo 4 --active false
ssh -p 23231 host repo webhook deliveries list demo 4
ssh -p 23231 host repo webhook deliveries get demo 4 550e8400-e29b-41d4-a716-446655440000These are not normally typed by humans. Git and Git LFS invoke them over SSH.
| Command | Purpose |
|---|---|
git-upload-pack REPO |
Clone/fetch over SSH |
git-upload-archive REPO |
Archive access over SSH |
git-receive-pack REPO |
Push over SSH |
git-lfs-authenticate REPO OPERATION |
Git LFS auth handshake |
git-lfs-transfer REPO OPERATION |
Git LFS transfer protocol |
If LFS is disabled, the LFS commands are not registered. If lfs.ssh_enabled is
false, git-lfs-transfer is not registered.
The Git transport URLs and repository workflow stay the same:
- SSH clone/push:
ssh://host:23231/<repo>.git - HTTP clone/push:
https://host/<repo>.git
strict=true does not disable token-based HTTP access. It does this:
- forces anonymous access to
no-access - disables keyless access
- clamps SSH timeouts
- clamps HTTP CORS to
http.public_url - keeps the stats listener on loopback when enabled
That means:
- SSH always requires an authorized key
- HTTP Git/LFS requires valid credentials
- access tokens still work for automation such as
pipe pipecan keep using internal HTTP with a token behind Caddy; no server-local TLS changes are required for that flow
With strict=true, a non-private repo is not anonymously readable. Today there
is no per-repo “public override” when global anonymous access is forced off.
This is intentional hardening. If you need anonymous read access for non-private repos, do not enable strict mode.
soft-serve did not ship with Gotify support. git-cone adds it.
Use it if you want the server to send notifications to Gotify for selected
events. This can be useful on its own, or alongside tools such as pipe.
When disabled, the server does not make notification network calls. When enabled, notification delivery failures do not block SSH, Git, HTTP, hooks, or pushes.
Current events emitted by the fork:
| Event | Trigger | Priority |
|---|---|---|
git-cone: new user |
A new user is created | 5 |
git-cone: webhook failure |
A webhook fails 3 times in a row | 5 |
git-cone: push to private repo |
A user pushes to a private repository | 3 |
git-cone: auth failure burst |
5 failed auth attempts from one IP in 60s | 7 |
What these mean in practice:
new user: a new account was createdwebhook failure: one webhook failed 3 times in a rowpush to private repo: someone pushed to a private repositoryauth failure burst: one IP accumulated 5 failed auth attempts in 60 seconds
YAML:
notify:
gotify:
enabled: true
url: "https://gotify.example.com"
token: "YOUR_GOTIFY_APP_TOKEN"
priority: 5Environment variables:
GIT_CONE_NOTIFY_GOTIFY_ENABLED=trueGIT_CONE_NOTIFY_GOTIFY_URL=https://gotify.example.comGIT_CONE_NOTIFY_GOTIFY_TOKEN=YOUR_GOTIFY_APP_TOKENGIT_CONE_NOTIFY_GOTIFY_PRIORITY=5
Compatibility variables with the old prefix also work:
SOFT_SERVE_NOTIFY_GOTIFY_ENABLEDSOFT_SERVE_NOTIFY_GOTIFY_URLSOFT_SERVE_NOTIFY_GOTIFY_TOKENSOFT_SERVE_NOTIFY_GOTIFY_PRIORITY
The old prefix support here is provided by git-cone's dual-prefix config
loader. It exists for migration convenience; it is not inherited Gotify support
from upstream.
services:
soft-serve:
image: ghcr.io/urutau-ltd/git-cone:latest
environment:
- GIT_CONE_NOTIFY_GOTIFY_ENABLED=true
- GIT_CONE_NOTIFY_GOTIFY_URL=http://gotify:80
- GIT_CONE_NOTIFY_GOTIFY_TOKEN=${GOTIFY_TOKEN}
- GIT_CONE_NOTIFY_GOTIFY_PRIORITY=5Notes:
- internal services such as
pipeshould talk directly tohttp://gotify:80 - avoid sending trusted service-to-service traffic through Anubis
- notification delivery failures do not block Git or SSH operations
For pipe, use an access token and internal HTTP:
http://pipe-bot:${PIPE_GIT_TOKEN}@soft-serve:23232
Recommended setup:
- Create a
pipe-botuser. - Grant it read-only or admin access to the repos it needs.
- Create a token with
ssh cone token create. - Store that token in your deployment env as
PIPE_GIT_TOKEN.
This keeps working with security.strict=true. Strict mode disables anonymous
and keyless access, but it does not disable HTTP token auth for Git/LFS.
If you also run Gotify, let pipe talk to http://gotify:80 directly on the
internal network. Do not put internal service-to-service traffic through Anubis.
Global hooks live under $GIT_CONE_DATA_PATH/hooks/. The server writes a sample
update.sample hook on first start.
The generated config.yaml is the canonical reference for the settings this
fork currently exposes.
Notable defaults:
db:
driver: "sqlite"
git:
listen_addr: "" # git:// disabled by default
hooks:
timeout: 30
security:
strict: false
notify:
gotify:
enabled: falseEnvironment prefixes:
GIT_CONE_*is preferredSOFT_SERVE_*remains supported for compatibility- if both are set,
GIT_CONE_*wins
Useful variables:
GIT_CONE_DATA_PATHGIT_CONE_INITIAL_ADMIN_KEYSGIT_CONE_SSH_PUBLIC_URLGIT_CONE_HTTP_PUBLIC_URLGIT_CONE_NAMEGIT_CONE_HOOKS_TIMEOUTGIT_CONE_SECURITY_STRICTGIT_CONE_NOTIFY_GOTIFY_ENABLEDGIT_CONE_NOTIFY_GOTIFY_URLGIT_CONE_NOTIFY_GOTIFY_TOKEN
This repo is Guix-friendly and includes a maintained manifest.scm.
make shell
make build
make test
make test-all
make imageFiles worth knowing:
manifest.scm: Guix dev environmentMakefile: common dev and CI entrypoints.pipe.yml: project pipelineinternal/cli/cli.go: shared CLI wiring forconeandsoftpkg/config/config.go: defaults and env loadingpkg/ssh/cmd/: SSH command handlers
The repo no longer ships systemd-centric packaging inherited from upstream. If you need host-managed services, optional examples live in docs/service-managers.md for:
- SysVinit
- OpenRC
- Runit
- GNU Shepherd

