Git LFS over Tor - Proof of Concept
Abstract
Git LFS (Large File Storage) is an extension to git as well as a specialized client. It aims to reduce the amount of space taken by a repository on an enduser machine, improve performance and make handling binary files more efficient.
Usecases
Somethines you will want to version non-text files in a git repository. Git being text-oriented this is usually a bad idea performance-wise. Yet the need could be: - AI models - Images and other media - Other blobs that your repo needs at deployment time
Architecture

As exposed, text files are versioned under git as normal, large files (registered with git lfs to be stored within it) are uploaded separately and replaced with placeholders containing their hash in the main repo. This allows the user to lazily download them based on need.
Forgejo setup
In order to support LFS in forgejo one can either update the configuration file as follows:
[server]
LFS_START_SERVER = true
or pass the following environment variables:
FORGEJO__server__LFS_START_SERVER = true;
This can be done directly in a docker-compose file (woking with the one from the forgejo tutorial:
services:
server:
image: codeberg.org/forgejo/forgejo:12
container_name: forgejo
environment:
- USER_UID=1000
- USER_GID=1000
- FORGEJO__webhook__ALLOWED_HOST_LIST="alerter" # add this to enable the alerter
- FORGEJO__server__LFS_START_SERVER=true;
restart: unless-stopped
networks:
- forgejo
- tor-forgejo
volumes:
- ./forgejo:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3009:3000"
- "127.0.0.1:2222:22"
# prevent dns leaks by setting dns server to a bogus address like localhost
dns:
- 127.0.0.1
alerter:
image: simplex-alerter:latest
restart: unless-stopped
volumes:
- /my/alerter/data/folder:/alerterconfig
networks:
- forgejo
tor-forgejo:
image: osminogin/tor-simple
restart: unless-stopped
container_name: tor-forgejo
volumes:
- ./tor-data:/var/lib/tor
- ./tor-data/torrc:/etc/tor
networks:
- tor-forgejo
networks:
tor-forgejo:
forgejo:
external: false
effects
LFS_START_SERVER: enables LFS support
Per-repo serverside configuration
No further configuration is required, now every repo will have git-lfs support.
Client-setup
For this demonstration we will be working from a new repository. Before starting you will need to have created it in forgejo.
To make use of a git-lfs enable repository the user needs to install the git-lfs extension. On debian this is done by running:
[user@devnode:~]$ sudo apt install git-lfs
We will now test git lfs in a new repository for tracking various type of binary files:
[user@devnode:~/Documents]$ git init git_lfs_test
Initialized empty Git repository in /home/user/Documents/git_lfs_test/.git/
[user@devnode:~/Documents]$ cd git_lfs_test/
[user@devnode:~/Documents/git_lfs_test]$ git config user.name "lfstestuser"
[user@devnode:~/Documents/git_lfs_test]$ git config user.email "user@dev.null"
[user@devnode:~/Documents/git_lfs_test]$ git config http.proxy "socks5h://127.0.0.1:9050"
[user@devnode:~/Documents/git_lfs_test]$ git lfs install --local
Updated Git hooks.
Git LFS initialized.
[user@devnode:~/Documents/git_lfs_test]$ git commit -m "root commit" --allow-empty
[master (root-commit) 5f3d67e] root commit
[user@devnode:~/Documents/git_lfs_test]$ git lfs track "*.png" "*.webp" "*.jpg" "*.avif" "*.mp4" "*.webm"
Tracking "*.png"
Tracking "*.webp"
Tracking "*.jpg"
Tracking "*.avif"
Tracking "*.mp4"
Tracking "*.webm"
[user@devnode:~/Documents/git_lfs_test]$ cat .gitattributes
*.png filter=lfs diff=lfs merge=lfs -text
[user@devnode:~/Documents/git_lfs_test]$ git add .gitattributes
[user@devnode:~/Documents/git_lfs_test]$ git commit -m "register pngs for tracking"
[master 4b1ffbd] register pngs for tracking
1 file changed, 1 insertion(+)
create mode 100644 .gitattributes
Now that we have created the .gitattributes file, git-lfs will know to replace png files with pointers to the selfsame and upload them separately.
[user@devnode:~/Documents/git_lfs_test]$ cp ~/Documents/opsec-blogposts/forgejo-anon/0.png .
[user@devnode:~/Documents/git_lfs_test]$ git add 0.png
[user@devnode:~/Documents/git_lfs_test]$ git commit -m "add png file"
[master cb9bfe3] add png file
1 file changed, 3 insertions(+)
create mode 100644 0.png
[user@devnode:~/Documents/git_lfs_test]$ git show HEAD
commit cb9bfe3487130a81246dffd0a6999556eb07bbe6 (HEAD -> master)
Author: lfstestuser <user@dev.null>
Date: Sat Aug 9 12:26:19 2025 +0200
add png file
diff --git a/0.png b/0.png
new file mode 100644
index 0000000..69b64e6
--- /dev/null
+++ b/0.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:afc78d10477909ac0dabd989141b66dc24cda3d07d903828b3d60da365ddef2f
+size 23701
As we can see, the binary file wasn't registered as is in the git repo, instead a pointer file was used.
We will also add multiple files to a single directory:
[user@devnode:~/Documents/git_lfs_test]$ mkdir various_files
[user@devnode:~/Documents/git_lfs_test]$ cp ../opsec-blogposts/forgejo-anon/1.png various_files/
[user@devnode:~/Documents/git_lfs_test]$ cp ../opsec-blogposts/forgejo-anon/2.png various_files/
[user@devnode:~/Documents/git_lfs_test]$ git add various_files/
[user@devnode:~/Documents/git_lfs_test]$ git commit -m "add multiple files"
[master 5f63094] add multiple files
2 files changed, 6 insertions(+)
create mode 100644 various_files/1.png
create mode 100644 various_files/2.png
[user@devnode:~/Documents/git_lfs_test]$ git push
Locking support detected on remote "origin". Consider enabling it with:
$ git config lfs.http://gitea2vyndora6imjfobo4rtnqylicqrncxjy34bb6v4zf56vlvz55yd.onion/midas/lfs_test.git/info/lfs.locksverify true
Uploading LFS objects: 100% (2/2), 51 KB | 0 B/s, done.
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 12 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 615 bytes | 615.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: . Processing 1 references
remote: Processed 1 references in total
To http://gitea2vyndora6imjfobo4rtnqylicqrncxjy34bb6v4zf56vlvz55yd.onion/midas/lfs_test.git
a43b58f..5f63094 master -> master
Now let's push this data over to forgejo:
[user@devnode:~/Documents/git_lfs_test]$ git remote add origin http://[redacted].onion/midas/lfs_test.git
[user@devnode:~/Documents/git_lfs_test]$ git push --set-upstream origin master
Locking support detected on remote "origin". Consider enabling it with:
$ git config lfs.http://[redacted].onion/midas/lfs_test.git/info/lfs.locksverify true
Uploading LFS objects: 100% (1/1), 24 KB | 0 B/s, done.
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 12 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (8/8), 760 bytes | 760.00 KiB/s, done.
Total 8 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: . Processing 1 references
remote: Processed 1 references in total
To http://[redacted].onion/midas/lfs_test.git
* [new branch] master -> master
branch 'master' set up to track 'origin/master'.
When opening the repository in the browser, the file will appear normally:

If one hovers over the data usage indication, they will see a breakdown of repo usage between git and lfs:

New contributor setup
As a new contributor, one would initially clone the existing repository:
[user@devnode:~/Documents]$ git clone -c http.proxy="socks5h://127.0.0.1:9050" http://[redacted].onion/midas/lfs_test.git gitlfstest2
Cloning into 'gitlfstest2'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 8 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (8/8), done.
Without doing any other setup, this is what happens regarding git-lfs files: they do not get downloaded with the repo
[user@devnode:~/Documents]$ cd gitlfstest2/
[user@devnode:~/Documents/gitlfstest2]$ ls
0.png
[user@devnode:~/Documents/gitlfstest2]$ cat 0.png
version https://git-lfs.github.com/spec/v1
oid sha256:afc78d10477909ac0dabd989141b66dc24cda3d07d903828b3d60da365ddef2f
size 23701
This is desired behavior, a user should only have to download the files they need.
Let's do a minimal configuration as a new contributor:
[user@devnode:~/Documents/gitlfstest2]$ git lfs install --local
Updated Git hooks.
Git LFS initialized.
[user@devnode:~/Documents/gitlfstest2]$ git lfs pull --include="0.png"
Downloading LFS objects: 100% (1/1), 24 KB | 0 B/s
[user@devnode:~/Documents/gitlfstest2]$ file 0.png
0.png: ISO Media, AVIF Image
[user@devnode:~/Documents/gitlfstest2]$ git lfs pull --include="various_files"
Downloading LFS objects: 100% (2/2), 51 KB | 0 B/s
We now have downloaded the png file (through the git lfs pull command), and can modify it;
We could have downloaded all files by simply running git lfs pull
[user@devnode:~/Documents/gitlfstest2]$ git config user.name "contributor"
[user@devnode:~/Documents/gitlfstest2]$ git config user.email "contributor@dev.null"
[user@devnode:~/Documents/gitlfstest2]$ cp ../opsec-blogposts/forgejo-anon/10.png 0.png
[user@devnode:~/Documents/gitlfstest2]$ git add 0.png
[user@devnode:~/Documents/gitlfstest2]$ git commit -m "change image"
[master d45dabb] change image
1 file changed, 2 insertions(+), 2 deletions(-)
[user@devnode:~/Documents/gitlfstest2]$ git show HEAD
commit d45dabb7527368e249e82b145d4775619faad50a (HEAD -> master)
Author: contributor <contributor@dev.null>
Date: Sat Aug 9 12:42:49 2025 +0200
change image
diff --git a/0.png b/0.png
index 69b64e6..92d7e5f 100644
--- a/0.png
+++ b/0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:afc78d10477909ac0dabd989141b66dc24cda3d07d903828b3d60da365ddef2f
-size 23701
+oid sha256:41ed41f5bd24c4e84c32b61617cde55a84ea0569f64f06c99863010897c1de1c
+size 12101
[user@devnode:~/Documents/gitlfstest2]$ git push
Locking support detected on remote "origin". Consider enabling it with:
$ git config lfs.https://gitea2vyndora6imjfobo4rtnqylicqrncxjy34bb6v4zf56vlvz55yd.onion/midas/lfs_test.git/info/lfs.locksverify true
Uploading LFS objects: 100% (1/1), 12 KB | 0 B/s, done.
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 382 bytes | 382.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://gitea2vyndora6imjfobo4rtnqylicqrncxjy34bb6v4zf56vlvz55yd.onion:2222/midas/lfs_test.git
cb9bfe3..d45dabb master -> master
Back in our first repository, if we pull the changes:
[user@devnode:~/Documents/gitlfstest2]$ cd ../git_lfs_test
[user@devnode:~/Documents/git_lfs_test]$ git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (3/3), 363 bytes | 363.00 KiB/s, done.
From ssh://gitea2vyndora6imjfobo4rtnqylicqrncxjy34bb6v4zf56vlvz55yd.onion:2222/midas/lfs_test
cb9bfe3..d45dabb master -> origin/master
Updating cb9bfe3..d45dabb
Fast-forward
0.png | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
[user@devnode:~/Documents/git_lfs_test]$ gwenview 0.png
We get the new file without having to do any lfs-related command, because we already told our local repo that we were interested in keeping this one local and updated:

Conclusion
Git-lfs has matured a lot and can be used to handle repository growth more effectively when non-text files represent a significant fraction of the checked in files. It gives the option to only download what files users are really interested in and only them.
Suggest changes
MulliganSecurity 2025-08-22
Donate XMR to the author:
82htErcFXbSigdhK9tbfMoJngZmjGtDUacQxxUFwSvtb9MY8uPSuYSGAuN1UvsXiXJ8BR9BVUUhgFBYDPvhrSmVkGneb91j