WIUNIX ACADEMY
MinIO after archival: a private S3 lab from pinned source
Page 2 of 4. Review the earlier steps and prerequisites before continuing.
YOUR SYSTEM. YOUR STEPS.
Where are you working?
Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.
USE YOUR OWN VALUES
Configure this tutorial
Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.
Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.
Set values for my environment6 editable values
LET’S BUILD
Verify archives and build frozen upstream source
The Go1.27.3 linux-amd64 toolchain checksum and both source-archive hashes were checked against actual downloads on2026-10-21. The server/client commits are pinned, not latest. Archives are extracted with the data filter, dependency integrity uses Go modules/go.sum, and builds have two-worker limits. Downloads, module cache and binaries remain only under /srv/academy-minio.
Network failure, hash mismatch, unavailable modules or compiler failure stops the process. Do not replace a failed URL with an unverified mirror or cached image. Hashes establish reviewed input identity, not absence of vulnerabilities or vendor support. A DEVELOPMENT version label is possible with a raw source build; the commit and build inputs identify it.
Inside the disposable container · terminal
set -eu
python3 - <<'PY'
import os,hashlib,tarfile,urllib.request,subprocess
from pathlib import Path
r=Path('/srv/academy-minio')
items=[
('go','https://go.dev/dl/go1.27.3.linux-amd64.tar.gz','65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40'),
('server','https://codeload.github.com/minio/minio/tar.gz/7aac2a2c5b7c882e68c1ce017d8256be2feea27f','71794c2df26aad0cc99e8421c58b7aa2dd55969f979b0e7d1e931042e9fabcd6'),
('client','https://codeload.github.com/minio/mc/tar.gz/77f82e18b5401a65958f1619df6ebb994634bd88','167415edd21bc29f5360943dac64272aa5cda0a39f3070b15cfeca671c43d975')]
for name,url,sha in items:
p=r/'downloads'/(name+'.tar.gz')
with p.open('xb') as f, urllib.request.urlopen(url,timeout=90) as response:
while block:=response.read(1024*1024): f.write(block)
assert hashlib.sha256(p.read_bytes()).hexdigest()==sha, name+' hash mismatch'
with tarfile.open(p) as t: t.extractall(r,filter='data')
print(name,'SHA256 verified')
env={**os.environ,'PATH':str(r/'go/bin')+':'+os.environ['PATH'],
'GOTOOLCHAIN':'local','CGO_ENABLED':'0','GOMAXPROCS':'2','GOPATH':str(r/'gopath')}
for directory,binary in [('minio-7aac2a2c5b7c882e68c1ce017d8256be2feea27f','minio'),('mc-77f82e18b5401a65958f1619df6ebb994634bd88','mc')]:
target=r/'bin'/binary
assert not target.exists() and not target.is_symlink()
subprocess.run(['go','build','-p','2','-trimpath','-o',str(target),'.'],cwd=r/directory,env=env,check=True)
print(binary,'built from pinned archived source')
PY
Three verified archives; minio and mc binaries built without installing host services.
LET’S BUILD
Create private credentials and client-scoped TLS trust
Generate unrelated random root/application keys into0600 files; no secret is printed or placed in argv. Each mc alias is written directly to its private config file. Do not use mc alias set with keys in a command, shell tracing or JSON debug output. Never put these files in Git, source packages, tickets or browser storage.
The certificate is self-signed, valid for two days and covers only127.0.0.1. It is trusted explicitly by these lab clients; the system trust store is untouched. The private-key file and full directory tree are sensitive. The command block starts only once: rerunning fails on exclusive credential files before certificate creation. This is not public ACME issuance or renewal.
Inside the disposable container · terminal
set -eu
python3 - <<'PY'
import os,json,secrets
from pathlib import Path
r=Path('/srv/academy-minio')
def private(path,content):
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'w',encoding='utf-8') as f: f.write(content)
for role in ('root','app'):
user=secrets.token_hex(12); password=secrets.token_urlsafe(36)
private(r/'private'/(role+'-user'),user)
private(r/'private'/(role+'-password'),password)
cfg={'version':'10','aliases':{'lab':{'url':'https://127.0.0.1:19000','accessKey':user,'secretKey':password,'api':'S3v4','path':'on'}}}
private(r/(role+'-client')/'config.json',json.dumps(cfg))
print('Random credentials written to exclusive0600 files; none printed')
PY
umask 077
openssl req -x509 -newkey rsa:3072 -sha256 -nodes -days 2 -subj '/CN=academy-minio-loopback' -addext 'subjectAltName=IP:127.0.0.1' -keyout '/srv/academy-minio/certs/private.key' -out '/srv/academy-minio/certs/public.crt'
python3 - <<'PY'
from pathlib import Path
r=Path('/srv/academy-minio')
for role in ('root','app'):
p=r/(role+'-client')/'certs'/'CAs';p.mkdir(parents=True,mode=0o700)
with (p/'lab.crt').open('xb') as f: f.write((r/'certs/public.crt').read_bytes())
print('Client-scoped certificate trust only; system CA store unchanged')
PY
Protected files exist; no credentials appear in terminal output; trust is limited to the lab certificate.
LET’S BUILD
Write the isolated server launcher
Open the precreated empty file, paste the next complete Python file and save. It reads root credentials through MinIO _FILE variables, binds both interfaces to loopback and records only this child PID. No systemd unit or automatic restart is installed.
Inside the disposable container · terminal
set -eu
nano '/srv/academy-minio/start.py'
Saved launcher targets /srv/academy-minio and two distinct loopback ports.
Protected-file credentials and separate listeners
File content only. The API is at19000, the console at19001. Keep the API endpoint in S3 clients; a console URL is not an S3 endpoint.
Inside the disposable container · terminal
import os,subprocess,socket,ssl,time,urllib.request
from pathlib import Path
r=Path('/srv/academy-minio')
assert not (r/'run/server.pid').exists()
for port in (19000,19001):
with socket.socket() as s: s.bind(('127.0.0.1',port))
env={**os.environ,'MINIO_ROOT_USER_FILE':str(r/'private/root-user'),
'MINIO_ROOT_PASSWORD_FILE':str(r/'private/root-password'),
'MINIO_BROWSER_REDIRECT_URL':'https://127.0.0.1:19001',
'MINIO_UPDATE':'off'}
# Never enable shell tracing, dump env, or print credentials.
with (r/'run/server.log').open('xb') as log:
p=subprocess.Popen([str(r/'bin/minio'),'server',str(r/'data'),
'--address','127.0.0.1:19000','--console-address','127.0.0.1:19001',
'--certs-dir',str(r/'certs')],env=env,stdin=subprocess.DEVNULL,
stdout=log,stderr=log,start_new_session=True)
with (r/'run/server.pid').open('x',encoding='utf-8') as f: f.write(str(p.pid))
ctx=ssl.create_default_context(cafile=str(r/'certs/public.crt'))
for attempt in range(60):
if p.poll() is not None: raise RuntimeError('Server exited; inspect private log locally')
try:
with urllib.request.urlopen('https://127.0.0.1:19000/minio/health/ready',context=ctx,timeout=2) as q:
assert q.status==200
break
except OSError: time.sleep(1)
else: raise RuntimeError('Ready probe timed out')
print('S3 API ready with verified TLS; separate loopback console')
LET’S BUILD
Start the child and verify trusted readiness
Run inside the same container. The launcher refuses an existing PID file or occupied port. Failed readiness is a failure to inspect, not a reason to start a second server. Logs remain private. Readiness200 proves this process can answer, not disk durability, S3 authorization or HA.
Inside the disposable container · terminal
set -eu
python3 '/srv/academy-minio/start.py'
curl --fail --cacert '/srv/academy-minio/certs/public.crt' \
+ 'https://127.0.0.1:19000/minio/health/ready'
Trusted TLS readiness200 from127.0.0.1:19000; console remains separate.
LET’S BUILD
Create a scoped provisioning program
Paste the following complete Go file into the precreated empty editor file. It uses official madmin-go and minio-go versions already locked in the server module. Root credentials stay in process memory; the application access key is never an argv argument or diagnostic line.
Inside the disposable container · terminal
set -eu
nano '/srv/academy-minio/provision.go'
Complete provision.go saved; no credential literals inserted.
One private bucket policy, no delete or admin grant
This application identity may list academy-objects and get/put its objects. It has no global bucket-list, deletion, historical-version, IAM or admin grant. academy-objects-outside is a negative-test bucket. Bucket existence and account checks prevent changing preexisting fixture identities; this program is not idempotent provisioning.
Inside the disposable container · terminal
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
func must(err error) { if err != nil { panic(err) } }
func main() {
r := "/srv/academy-minio"
read := func(n string) string { b,e:=os.ReadFile(filepath.Join(r,n));must(e);return string(b) }
roots:=x509.NewCertPool()
if !roots.AppendCertsFromPEM([]byte(read("certs/public.crt"))) { panic("CA rejected") }
transport:=&http.Transport{TLSClientConfig:&tls.Config{RootCAs:roots,MinVersion:tls.VersionTLS12}}
endpoint:="127.0.0.1:19000"
root,secret:=read("private/root-user"),read("private/root-password")
app,appSecret:=read("private/app-user"),read("private/app-password")
admin,e:=madmin.New(endpoint,root,secret,true);must(e);admin.SetCustomTransport(transport)
s3,e:=minio.New(endpoint,&minio.Options{Creds:credentials.NewStaticV4(root,secret,""),Secure:true,Transport:transport});must(e)
ctx:=context.Background()
// Refuse existing names rather than changing an existing bucket or account.
for _,bucket:=range []string{"academy-objects","academy-objects-outside"} {
exists,e:=s3.BucketExists(ctx,bucket);must(e);if exists { panic("Bucket exists; stop") }
}
users,e:=admin.ListUsers(ctx);must(e);if _,ok:=users[app];ok {panic("Account exists; stop")}
for _,bucket:=range []string{"academy-objects","academy-objects-outside"} {must(s3.MakeBucket(ctx,bucket,minio.MakeBucketOptions{}))}
policy:=map[string]interface{}{"Version":"2012-10-17","Statement":[]interface{}{
map[string]interface{}{"Effect":"Allow","Action":[]string{"s3:ListBucket","s3:GetBucketLocation"},"Resource":[]string{"arn:aws:s3:::academy-objects"}},
map[string]interface{}{"Effect":"Allow","Action":[]string{"s3:PutObject","s3:GetObject"},"Resource":[]string{"arn:aws:s3:::academy-objects/*"}},
}}
body,e:=json.Marshal(policy);must(e)
must(admin.AddCannedPolicy(ctx,"academy-objects-rw",body))
must(admin.AddUser(ctx,app,appSecret))
_,e=admin.AttachPolicy(ctx,madmin.PolicyAssociationReq{User:app,Policies:[]string{"academy-objects-rw"}});must(e)
fmt.Println("Two new private buckets and one bucket-scoped application identity created")
}